From 321e8be45b3f9659d1c053db39ae30fa51a7f5ef Mon Sep 17 00:00:00 2001 From: Madraka <8898640+Madraka@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:06:29 +0300 Subject: [PATCH 1/3] feat(contracts,core-ts): collections stage 3 contract and core (RFC-0042 step 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Contract: CollectionSettings.listFields, expiry and structuredData (sources by kind: field, entry, site, text; EntryValueProperty and SiteValueProperty as named $defs), contentList where/orderBy (FieldCondition, FieldOrder); 4 valid and 44 invalid fixtures; TS and Rust regenerated. - @cloudhal/core-ts/structured-data: the closed table for Event, JobPosting, Product and Recipe, checkStructuredData, structuredDataFor (required values, vocabularies, ISO 4217, 32 KB). - @cloudhal/core-ts/fields: the shared query evaluator (query.ts) and fieldGateInstant / expiryInstant (gate.ts, Intl only), with an oracle test against Temporal across 17 zones. - ADR-0044: the constant source kind is `text`, as in RFC-0042 §1. No behaviour change. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com> --- .changeset/collections-stage3-contract.md | 6 + crates/cloudhal-schema/src/generated/page.rs | 376 ++ .../cloudhal-schema/src/generated/section.rs | 376 ++ crates/cloudhal-schema/src/generated/site.rs | 905 ++- docs/adr/0044-collections-stage-3.md | 2 +- .../content-list-order-direction-up.json | 26 + .../content-list-order-extra-property.json | 27 + .../content-list-order-without-direction.json | 25 + .../section/content-list-where-bad-field.json | 26 + .../section/content-list-where-empty.json | 15 + .../content-list-where-extra-property.json | 27 + .../section/content-list-where-op-like.json | 26 + .../section/content-list-where-too-many.json | 64 + .../content-list-where-value-empty.json | 26 + .../content-list-where-value-number.json | 26 + .../content-list-where-value-too-long.json | 26 + .../content-list-where-values-duplicate.json | 26 + .../content-list-where-values-empty.json | 26 + .../content-list-where-values-too-many.json | 48 + .../content-list-where-without-field.json | 25 + .../content-list-where-without-op.json | 25 + .../invalid/site/expiry-action-delete.json | 210 + .../invalid/site/expiry-extra-property.json | 211 + .../invalid/site/expiry-field-kebab.json | 210 + .../invalid/site/expiry-grace-fraction.json | 210 + .../invalid/site/expiry-grace-negative.json | 210 + .../invalid/site/expiry-grace-too-long.json | 210 + .../invalid/site/expiry-without-action.json | 209 + .../invalid/site/list-fields-bad-key.json | 210 + .../invalid/site/list-fields-duplicate.json | 210 + .../invalid/site/list-fields-empty.json | 210 + .../invalid/site/list-fields-too-many.json | 210 + .../structured-data-empty-properties.json | 157 + ...tructured-data-entry-unknown-property.json | 210 + .../site/structured-data-extra-property.json | 211 + .../site/structured-data-faq-page.json | 210 + .../site/structured-data-field-missing.json | 209 + .../structured-data-field-with-value.json | 211 + .../site/structured-data-kind-html.json | 214 + .../site/structured-data-path-digit.json | 214 + .../site/structured-data-path-too-deep.json | 214 + .../site/structured-data-path-upper.json | 214 + .../structured-data-site-entry-property.json | 210 + ...structured-data-site-unknown-property.json | 210 + .../structured-data-text-control-char.json | 214 + .../site/structured-data-text-empty.json | 214 + .../site/structured-data-text-too-long.json | 214 + .../structured-data-too-many-properties.json | 322 + .../site/structured-data-without-type.json | 209 + .../valid/section/content-list-where-ops.json | 32 + .../valid/section/content-list-where.json | 23 + .../valid/site/collection-stage3-presets.json | 152 + .../valid/site/collection-stage3.json | 207 + .../core-contracts/schemas/common.schema.json | 50 + .../schemas/section.schema.json | 10 +- .../core-contracts/schemas/site.schema.json | 145 + .../core-contracts/src/generated/index.ts | 2 +- packages/core-contracts/src/generated/page.ts | 217 + .../core-contracts/src/generated/schemas.ts | 434 ++ .../core-contracts/src/generated/section.ts | 211 + packages/core-contracts/src/generated/site.ts | 313 + .../src/generated/validators.js | 5260 +++++++++++------ packages/core-ts/package.json | 7 +- packages/core-ts/src/fields/conversions.ts | 4 +- packages/core-ts/src/fields/gate.ts | 119 + packages/core-ts/src/fields/index.ts | 21 +- packages/core-ts/src/fields/query.ts | 371 ++ packages/core-ts/src/structured-data/build.ts | 182 + packages/core-ts/src/structured-data/check.ts | 229 + packages/core-ts/src/structured-data/index.ts | 45 + packages/core-ts/src/structured-data/table.ts | 311 + .../core-ts/src/structured-data/values.ts | 268 + packages/core-ts/test/fields-gate.test.ts | 233 + packages/core-ts/test/fields-query.test.ts | 347 ++ packages/core-ts/test/structured-data.test.ts | 601 ++ .../core-ts/test/worker-time-free.test.ts | 3 +- 76 files changed, 16168 insertions(+), 1725 deletions(-) create mode 100644 .changeset/collections-stage3-contract.md create mode 100644 packages/core-contracts/fixtures/invalid/section/content-list-order-direction-up.json create mode 100644 packages/core-contracts/fixtures/invalid/section/content-list-order-extra-property.json create mode 100644 packages/core-contracts/fixtures/invalid/section/content-list-order-without-direction.json create mode 100644 packages/core-contracts/fixtures/invalid/section/content-list-where-bad-field.json create mode 100644 packages/core-contracts/fixtures/invalid/section/content-list-where-empty.json create mode 100644 packages/core-contracts/fixtures/invalid/section/content-list-where-extra-property.json create mode 100644 packages/core-contracts/fixtures/invalid/section/content-list-where-op-like.json create mode 100644 packages/core-contracts/fixtures/invalid/section/content-list-where-too-many.json create mode 100644 packages/core-contracts/fixtures/invalid/section/content-list-where-value-empty.json create mode 100644 packages/core-contracts/fixtures/invalid/section/content-list-where-value-number.json create mode 100644 packages/core-contracts/fixtures/invalid/section/content-list-where-value-too-long.json create mode 100644 packages/core-contracts/fixtures/invalid/section/content-list-where-values-duplicate.json create mode 100644 packages/core-contracts/fixtures/invalid/section/content-list-where-values-empty.json create mode 100644 packages/core-contracts/fixtures/invalid/section/content-list-where-values-too-many.json create mode 100644 packages/core-contracts/fixtures/invalid/section/content-list-where-without-field.json create mode 100644 packages/core-contracts/fixtures/invalid/section/content-list-where-without-op.json create mode 100644 packages/core-contracts/fixtures/invalid/site/expiry-action-delete.json create mode 100644 packages/core-contracts/fixtures/invalid/site/expiry-extra-property.json create mode 100644 packages/core-contracts/fixtures/invalid/site/expiry-field-kebab.json create mode 100644 packages/core-contracts/fixtures/invalid/site/expiry-grace-fraction.json create mode 100644 packages/core-contracts/fixtures/invalid/site/expiry-grace-negative.json create mode 100644 packages/core-contracts/fixtures/invalid/site/expiry-grace-too-long.json create mode 100644 packages/core-contracts/fixtures/invalid/site/expiry-without-action.json create mode 100644 packages/core-contracts/fixtures/invalid/site/list-fields-bad-key.json create mode 100644 packages/core-contracts/fixtures/invalid/site/list-fields-duplicate.json create mode 100644 packages/core-contracts/fixtures/invalid/site/list-fields-empty.json create mode 100644 packages/core-contracts/fixtures/invalid/site/list-fields-too-many.json create mode 100644 packages/core-contracts/fixtures/invalid/site/structured-data-empty-properties.json create mode 100644 packages/core-contracts/fixtures/invalid/site/structured-data-entry-unknown-property.json create mode 100644 packages/core-contracts/fixtures/invalid/site/structured-data-extra-property.json create mode 100644 packages/core-contracts/fixtures/invalid/site/structured-data-faq-page.json create mode 100644 packages/core-contracts/fixtures/invalid/site/structured-data-field-missing.json create mode 100644 packages/core-contracts/fixtures/invalid/site/structured-data-field-with-value.json create mode 100644 packages/core-contracts/fixtures/invalid/site/structured-data-kind-html.json create mode 100644 packages/core-contracts/fixtures/invalid/site/structured-data-path-digit.json create mode 100644 packages/core-contracts/fixtures/invalid/site/structured-data-path-too-deep.json create mode 100644 packages/core-contracts/fixtures/invalid/site/structured-data-path-upper.json create mode 100644 packages/core-contracts/fixtures/invalid/site/structured-data-site-entry-property.json create mode 100644 packages/core-contracts/fixtures/invalid/site/structured-data-site-unknown-property.json create mode 100644 packages/core-contracts/fixtures/invalid/site/structured-data-text-control-char.json create mode 100644 packages/core-contracts/fixtures/invalid/site/structured-data-text-empty.json create mode 100644 packages/core-contracts/fixtures/invalid/site/structured-data-text-too-long.json create mode 100644 packages/core-contracts/fixtures/invalid/site/structured-data-too-many-properties.json create mode 100644 packages/core-contracts/fixtures/invalid/site/structured-data-without-type.json create mode 100644 packages/core-contracts/fixtures/valid/section/content-list-where-ops.json create mode 100644 packages/core-contracts/fixtures/valid/section/content-list-where.json create mode 100644 packages/core-contracts/fixtures/valid/site/collection-stage3-presets.json create mode 100644 packages/core-contracts/fixtures/valid/site/collection-stage3.json create mode 100644 packages/core-ts/src/fields/gate.ts create mode 100644 packages/core-ts/src/fields/query.ts create mode 100644 packages/core-ts/src/structured-data/build.ts create mode 100644 packages/core-ts/src/structured-data/check.ts create mode 100644 packages/core-ts/src/structured-data/index.ts create mode 100644 packages/core-ts/src/structured-data/table.ts create mode 100644 packages/core-ts/src/structured-data/values.ts create mode 100644 packages/core-ts/test/fields-gate.test.ts create mode 100644 packages/core-ts/test/fields-query.test.ts create mode 100644 packages/core-ts/test/structured-data.test.ts diff --git a/.changeset/collections-stage3-contract.md b/.changeset/collections-stage3-contract.md new file mode 100644 index 00000000..043151c8 --- /dev/null +++ b/.changeset/collections-stage3-contract.md @@ -0,0 +1,6 @@ +--- +"@cloudhal/core-contracts": minor +"@cloudhal/core-ts": minor +--- + +Collections stage 3 contract and pure core (RFC-0042 step 1, ADR-0044). `CollectionSettings` gains optional `listFields` (up to 4 field keys), `expiry` (`{ field, action: "unlist" | "hide", afterMinutes? }`) and `structuredData` (`{ type: "Event" | "JobPosting" | "Product" | "Recipe", properties }`, each property sourced by `kind`: `field`, `entry`, `site` or a plain-text `text` constant); `contentList` sections gain `where` (up to 4 `FieldCondition`s) and `orderBy` (`FieldOrder`), both new in `common.schema.json`. New `@cloudhal/core-ts/structured-data`: the closed preset table, `checkStructuredData` (save-time mapping check) and `structuredDataFor` (an entry's JSON-LD, none when a required value is missing, capped at 32 KB). `@cloudhal/core-ts/fields` adds the listing evaluator (`checkFieldQuery`, `parseFieldQuery`, `matchesFieldQuery`, `compareByField`) and the derived expiry gate (`fieldGateInstant`, `expiryInstant`, `effectiveUntil`, `dayStartIn`). Nothing reads them yet: no behaviour change. diff --git a/crates/cloudhal-schema/src/generated/page.rs b/crates/cloudhal-schema/src/generated/page.rs index 413a74c5..f2fdfed7 100644 --- a/crates/cloudhal-schema/src/generated/page.rs +++ b/crates/cloudhal-schema/src/generated/page.rs @@ -371,6 +371,370 @@ impl<'de> ::serde::Deserialize<'de> for BodyProfile { .map_err(|e| ::custom(e.to_string())) } } +#[doc = "RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`."] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct FieldCondition { + #[doc = "A field key (site.schema.json FieldKey's pattern, repeated)."] + pub field: FieldConditionField, + pub op: FieldConditionOp, + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + pub value: ::std::option::Option, + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + pub values: ::std::option::Option<::std::vec::Vec>, +} +#[doc = "A field key (site.schema.json FieldKey's pattern, repeated)."] +#[derive(:: serde :: Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct FieldConditionField(::std::string::String); +impl ::std::ops::Deref for FieldConditionField { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: FieldConditionField) -> Self { + value.0 + } +} +impl ::std::str::FromStr for FieldConditionField { + 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 FieldConditionField { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for FieldConditionField { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for FieldConditionField { + 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 = "`FieldConditionOp`"] +#[derive( + :: serde :: Deserialize, + :: serde :: Serialize, + Clone, + Copy, + Debug, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, +)] +pub enum FieldConditionOp { + #[serde(rename = "eq")] + Eq, + #[serde(rename = "ne")] + Ne, + #[serde(rename = "lt")] + Lt, + #[serde(rename = "lte")] + Lte, + #[serde(rename = "gt")] + Gt, + #[serde(rename = "gte")] + Gte, + #[serde(rename = "in")] + In, + #[serde(rename = "set")] + Set, + #[serde(rename = "unset")] + Unset, +} +impl ::std::fmt::Display for FieldConditionOp { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match *self { + Self::Eq => f.write_str("eq"), + Self::Ne => f.write_str("ne"), + Self::Lt => f.write_str("lt"), + Self::Lte => f.write_str("lte"), + Self::Gt => f.write_str("gt"), + Self::Gte => f.write_str("gte"), + Self::In => f.write_str("in"), + Self::Set => f.write_str("set"), + Self::Unset => f.write_str("unset"), + } + } +} +impl ::std::str::FromStr for FieldConditionOp { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> ::std::result::Result { + match value { + "eq" => Ok(Self::Eq), + "ne" => Ok(Self::Ne), + "lt" => Ok(Self::Lt), + "lte" => Ok(Self::Lte), + "gt" => Ok(Self::Gt), + "gte" => Ok(Self::Gte), + "in" => Ok(Self::In), + "set" => Ok(Self::Set), + "unset" => Ok(Self::Unset), + _ => Err("invalid value".into()), + } + } +} +impl ::std::convert::TryFrom<&str> for FieldConditionOp { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for FieldConditionOp { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +#[doc = "`FieldConditionValue`"] +#[derive(:: serde :: Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct FieldConditionValue(::std::string::String); +impl ::std::ops::Deref for FieldConditionValue { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: FieldConditionValue) -> Self { + value.0 + } +} +impl ::std::str::FromStr for FieldConditionValue { + 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 FieldConditionValue { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for FieldConditionValue { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for FieldConditionValue { + 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 = "`FieldConditionValuesItem`"] +#[derive(:: serde :: Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct FieldConditionValuesItem(::std::string::String); +impl ::std::ops::Deref for FieldConditionValuesItem { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: FieldConditionValuesItem) -> Self { + value.0 + } +} +impl ::std::str::FromStr for FieldConditionValuesItem { + 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 FieldConditionValuesItem { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for FieldConditionValuesItem { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for FieldConditionValuesItem { + 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 = "RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last."] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct FieldOrder { + pub direction: FieldOrderDirection, + #[doc = "A field key (site.schema.json FieldKey's pattern, repeated)."] + pub field: FieldOrderField, +} +#[doc = "`FieldOrderDirection`"] +#[derive( + :: serde :: Deserialize, + :: serde :: Serialize, + Clone, + Copy, + Debug, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, +)] +pub enum FieldOrderDirection { + #[serde(rename = "asc")] + Asc, + #[serde(rename = "desc")] + Desc, +} +impl ::std::fmt::Display for FieldOrderDirection { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match *self { + Self::Asc => f.write_str("asc"), + Self::Desc => f.write_str("desc"), + } + } +} +impl ::std::str::FromStr for FieldOrderDirection { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> ::std::result::Result { + match value { + "asc" => Ok(Self::Asc), + "desc" => Ok(Self::Desc), + _ => Err("invalid value".into()), + } + } +} +impl ::std::convert::TryFrom<&str> for FieldOrderDirection { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for FieldOrderDirection { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +#[doc = "A field key (site.schema.json FieldKey's pattern, repeated)."] +#[derive(:: serde :: Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct FieldOrderField(::std::string::String); +impl ::std::ops::Deref for FieldOrderField { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: FieldOrderField) -> Self { + value.0 + } +} +impl ::std::str::FromStr for FieldOrderField { + 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 FieldOrderField { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for FieldOrderField { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for FieldOrderField { + 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 = "Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles."] #[derive(:: serde :: Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] #[serde(transparent)] @@ -1251,7 +1615,19 @@ pub enum Section { filter: ::std::option::Option, id: Id, limit: ::std::num::NonZeroU64, + #[serde( + rename = "orderBy", + skip_serializing_if = "::std::option::Option::is_none" + )] + order_by: ::std::option::Option, sort: SectionSort, + #[doc = "RFC-0042: conditions on the collection's list fields, ANDed."] + #[serde( + rename = "where", + default, + skip_serializing_if = "::std::vec::Vec::is_empty" + )] + where_: ::std::vec::Vec, }, #[doc = "ComponentSection\n\nA 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."] #[serde(rename = "component")] diff --git a/crates/cloudhal-schema/src/generated/section.rs b/crates/cloudhal-schema/src/generated/section.rs index 91701f0b..bd70211c 100644 --- a/crates/cloudhal-schema/src/generated/section.rs +++ b/crates/cloudhal-schema/src/generated/section.rs @@ -522,6 +522,370 @@ impl ::std::convert::TryFrom<::std::string::String> for ContentListSectionSort { value.parse() } } +#[doc = "RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`."] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct FieldCondition { + #[doc = "A field key (site.schema.json FieldKey's pattern, repeated)."] + pub field: FieldConditionField, + pub op: FieldConditionOp, + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + pub value: ::std::option::Option, + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + pub values: ::std::option::Option<::std::vec::Vec>, +} +#[doc = "A field key (site.schema.json FieldKey's pattern, repeated)."] +#[derive(:: serde :: Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct FieldConditionField(::std::string::String); +impl ::std::ops::Deref for FieldConditionField { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: FieldConditionField) -> Self { + value.0 + } +} +impl ::std::str::FromStr for FieldConditionField { + 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 FieldConditionField { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for FieldConditionField { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for FieldConditionField { + 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 = "`FieldConditionOp`"] +#[derive( + :: serde :: Deserialize, + :: serde :: Serialize, + Clone, + Copy, + Debug, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, +)] +pub enum FieldConditionOp { + #[serde(rename = "eq")] + Eq, + #[serde(rename = "ne")] + Ne, + #[serde(rename = "lt")] + Lt, + #[serde(rename = "lte")] + Lte, + #[serde(rename = "gt")] + Gt, + #[serde(rename = "gte")] + Gte, + #[serde(rename = "in")] + In, + #[serde(rename = "set")] + Set, + #[serde(rename = "unset")] + Unset, +} +impl ::std::fmt::Display for FieldConditionOp { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match *self { + Self::Eq => f.write_str("eq"), + Self::Ne => f.write_str("ne"), + Self::Lt => f.write_str("lt"), + Self::Lte => f.write_str("lte"), + Self::Gt => f.write_str("gt"), + Self::Gte => f.write_str("gte"), + Self::In => f.write_str("in"), + Self::Set => f.write_str("set"), + Self::Unset => f.write_str("unset"), + } + } +} +impl ::std::str::FromStr for FieldConditionOp { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> ::std::result::Result { + match value { + "eq" => Ok(Self::Eq), + "ne" => Ok(Self::Ne), + "lt" => Ok(Self::Lt), + "lte" => Ok(Self::Lte), + "gt" => Ok(Self::Gt), + "gte" => Ok(Self::Gte), + "in" => Ok(Self::In), + "set" => Ok(Self::Set), + "unset" => Ok(Self::Unset), + _ => Err("invalid value".into()), + } + } +} +impl ::std::convert::TryFrom<&str> for FieldConditionOp { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for FieldConditionOp { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +#[doc = "`FieldConditionValue`"] +#[derive(:: serde :: Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct FieldConditionValue(::std::string::String); +impl ::std::ops::Deref for FieldConditionValue { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: FieldConditionValue) -> Self { + value.0 + } +} +impl ::std::str::FromStr for FieldConditionValue { + 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 FieldConditionValue { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for FieldConditionValue { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for FieldConditionValue { + 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 = "`FieldConditionValuesItem`"] +#[derive(:: serde :: Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct FieldConditionValuesItem(::std::string::String); +impl ::std::ops::Deref for FieldConditionValuesItem { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: FieldConditionValuesItem) -> Self { + value.0 + } +} +impl ::std::str::FromStr for FieldConditionValuesItem { + 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 FieldConditionValuesItem { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for FieldConditionValuesItem { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for FieldConditionValuesItem { + 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 = "RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last."] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct FieldOrder { + pub direction: FieldOrderDirection, + #[doc = "A field key (site.schema.json FieldKey's pattern, repeated)."] + pub field: FieldOrderField, +} +#[doc = "`FieldOrderDirection`"] +#[derive( + :: serde :: Deserialize, + :: serde :: Serialize, + Clone, + Copy, + Debug, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, +)] +pub enum FieldOrderDirection { + #[serde(rename = "asc")] + Asc, + #[serde(rename = "desc")] + Desc, +} +impl ::std::fmt::Display for FieldOrderDirection { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match *self { + Self::Asc => f.write_str("asc"), + Self::Desc => f.write_str("desc"), + } + } +} +impl ::std::str::FromStr for FieldOrderDirection { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> ::std::result::Result { + match value { + "asc" => Ok(Self::Asc), + "desc" => Ok(Self::Desc), + _ => Err("invalid value".into()), + } + } +} +impl ::std::convert::TryFrom<&str> for FieldOrderDirection { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for FieldOrderDirection { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +#[doc = "A field key (site.schema.json FieldKey's pattern, repeated)."] +#[derive(:: serde :: Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct FieldOrderField(::std::string::String); +impl ::std::ops::Deref for FieldOrderField { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: FieldOrderField) -> Self { + value.0 + } +} +impl ::std::str::FromStr for FieldOrderField { + 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 FieldOrderField { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for FieldOrderField { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for FieldOrderField { + 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 = "`HeroSectionHeading`"] #[derive(:: serde :: Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] #[serde(transparent)] @@ -1115,7 +1479,19 @@ pub enum Section { filter: ::std::option::Option, id: Id, limit: ::std::num::NonZeroU64, + #[serde( + rename = "orderBy", + skip_serializing_if = "::std::option::Option::is_none" + )] + order_by: ::std::option::Option, sort: ContentListSectionSort, + #[doc = "RFC-0042: conditions on the collection's list fields, ANDed."] + #[serde( + rename = "where", + default, + skip_serializing_if = "::std::vec::Vec::is_empty" + )] + where_: ::std::vec::Vec, }, #[doc = "ComponentSection\n\nA 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."] #[serde(rename = "component")] diff --git a/crates/cloudhal-schema/src/generated/site.rs b/crates/cloudhal-schema/src/generated/site.rs index 08d0a829..3c28a124 100644 --- a/crates/cloudhal-schema/src/generated/site.rs +++ b/crates/cloudhal-schema/src/generated/site.rs @@ -1657,6 +1657,71 @@ impl ::std::convert::TryFrom<::std::string::String> for ChromeHeaderWidth { value.parse() } } +#[doc = "RFC-0042: a time gate every entry derives from one of its date or datetime fields, checked at render time like Availability.until. A date ends at the start of the next day in the site's time zone."] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct CollectionExpiry { + #[doc = "unlist: the page stays, the entry leaves listings, feeds, the sitemap and search. hide: every public route answers 404."] + pub action: CollectionExpiryAction, + #[doc = "Grace after the field's instant. Absent: 0."] + #[serde( + rename = "afterMinutes", + skip_serializing_if = "::std::option::Option::is_none" + )] + pub after_minutes: ::std::option::Option, + pub field: FieldKey, +} +#[doc = "unlist: the page stays, the entry leaves listings, feeds, the sitemap and search. hide: every public route answers 404."] +#[derive( + :: serde :: Deserialize, + :: serde :: Serialize, + Clone, + Copy, + Debug, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, +)] +pub enum CollectionExpiryAction { + #[serde(rename = "unlist")] + Unlist, + #[serde(rename = "hide")] + Hide, +} +impl ::std::fmt::Display for CollectionExpiryAction { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match *self { + Self::Unlist => f.write_str("unlist"), + Self::Hide => f.write_str("hide"), + } + } +} +impl ::std::str::FromStr for CollectionExpiryAction { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> ::std::result::Result { + match value { + "unlist" => Ok(Self::Unlist), + "hide" => Ok(Self::Hide), + _ => Err("invalid value".into()), + } + } +} +impl ::std::convert::TryFrom<&str> for CollectionExpiryAction { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for CollectionExpiryAction { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} #[doc = "`CollectionLabel`"] #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, PartialEq)] #[serde(deny_unknown_fields)] @@ -1826,6 +1891,8 @@ impl<'de> ::serde::Deserialize<'de> for CollectionLabelSingular { #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, Default, PartialEq)] #[serde(deny_unknown_fields)] pub struct CollectionSettings { + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + pub expiry: ::std::option::Option, #[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, @@ -1841,6 +1908,12 @@ pub struct CollectionSettings { skip_serializing_if = ":: std :: collections :: HashMap::is_empty" )] pub labels: ::std::collections::HashMap, + #[doc = "RFC-0042: fields that listings, queries, Studio columns and MCP may filter and sort by, in column order. Keys of this collection's fields (checked in code)."] + #[serde( + rename = "listFields", + skip_serializing_if = "::std::option::Option::is_none" + )] + pub list_fields: ::std::option::Option<::std::vec::Vec>, #[doc = "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}."] #[serde(skip_serializing_if = "::std::option::Option::is_none")] pub permalink: ::std::option::Option, @@ -1850,6 +1923,11 @@ pub struct CollectionSettings { #[doc = "RFC-0040: default order where none is given (Studio's list, entry queries without sort, MCP list_entries). Absent: newest."] #[serde(skip_serializing_if = "::std::option::Option::is_none")] pub sort: ::std::option::Option, + #[serde( + rename = "structuredData", + skip_serializing_if = "::std::option::Option::is_none" + )] + pub structured_data: ::std::option::Option, } #[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."] #[derive(:: serde :: Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] @@ -2204,6 +2282,328 @@ impl<'de> ::serde::Deserialize<'de> for DomainHostname { }) } } +#[doc = "RFC-0042: entry properties a structured-data property may read."] +#[derive( + :: serde :: Deserialize, + :: serde :: Serialize, + Clone, + Copy, + Debug, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, +)] +pub enum EntryValueProperty { + #[serde(rename = "title")] + Title, + #[serde(rename = "summary")] + Summary, + #[serde(rename = "body")] + Body, + #[serde(rename = "coverImage")] + CoverImage, + #[serde(rename = "url")] + Url, + #[serde(rename = "publishedAt")] + PublishedAt, + #[serde(rename = "updatedAt")] + UpdatedAt, +} +impl ::std::fmt::Display for EntryValueProperty { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match *self { + Self::Title => f.write_str("title"), + Self::Summary => f.write_str("summary"), + Self::Body => f.write_str("body"), + Self::CoverImage => f.write_str("coverImage"), + Self::Url => f.write_str("url"), + Self::PublishedAt => f.write_str("publishedAt"), + Self::UpdatedAt => f.write_str("updatedAt"), + } + } +} +impl ::std::str::FromStr for EntryValueProperty { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> ::std::result::Result { + match value { + "title" => Ok(Self::Title), + "summary" => Ok(Self::Summary), + "body" => Ok(Self::Body), + "coverImage" => Ok(Self::CoverImage), + "url" => Ok(Self::Url), + "publishedAt" => Ok(Self::PublishedAt), + "updatedAt" => Ok(Self::UpdatedAt), + _ => Err("invalid value".into()), + } + } +} +impl ::std::convert::TryFrom<&str> for EntryValueProperty { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for EntryValueProperty { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +#[doc = "RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`."] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct FieldCondition { + #[doc = "A field key (site.schema.json FieldKey's pattern, repeated)."] + pub field: FieldConditionField, + pub op: FieldConditionOp, + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + pub value: ::std::option::Option, + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + pub values: ::std::option::Option<::std::vec::Vec>, +} +#[doc = "A field key (site.schema.json FieldKey's pattern, repeated)."] +#[derive(:: serde :: Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct FieldConditionField(::std::string::String); +impl ::std::ops::Deref for FieldConditionField { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: FieldConditionField) -> Self { + value.0 + } +} +impl ::std::str::FromStr for FieldConditionField { + 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 FieldConditionField { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for FieldConditionField { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for FieldConditionField { + 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 = "`FieldConditionOp`"] +#[derive( + :: serde :: Deserialize, + :: serde :: Serialize, + Clone, + Copy, + Debug, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, +)] +pub enum FieldConditionOp { + #[serde(rename = "eq")] + Eq, + #[serde(rename = "ne")] + Ne, + #[serde(rename = "lt")] + Lt, + #[serde(rename = "lte")] + Lte, + #[serde(rename = "gt")] + Gt, + #[serde(rename = "gte")] + Gte, + #[serde(rename = "in")] + In, + #[serde(rename = "set")] + Set, + #[serde(rename = "unset")] + Unset, +} +impl ::std::fmt::Display for FieldConditionOp { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match *self { + Self::Eq => f.write_str("eq"), + Self::Ne => f.write_str("ne"), + Self::Lt => f.write_str("lt"), + Self::Lte => f.write_str("lte"), + Self::Gt => f.write_str("gt"), + Self::Gte => f.write_str("gte"), + Self::In => f.write_str("in"), + Self::Set => f.write_str("set"), + Self::Unset => f.write_str("unset"), + } + } +} +impl ::std::str::FromStr for FieldConditionOp { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> ::std::result::Result { + match value { + "eq" => Ok(Self::Eq), + "ne" => Ok(Self::Ne), + "lt" => Ok(Self::Lt), + "lte" => Ok(Self::Lte), + "gt" => Ok(Self::Gt), + "gte" => Ok(Self::Gte), + "in" => Ok(Self::In), + "set" => Ok(Self::Set), + "unset" => Ok(Self::Unset), + _ => Err("invalid value".into()), + } + } +} +impl ::std::convert::TryFrom<&str> for FieldConditionOp { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for FieldConditionOp { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +#[doc = "`FieldConditionValue`"] +#[derive(:: serde :: Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct FieldConditionValue(::std::string::String); +impl ::std::ops::Deref for FieldConditionValue { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: FieldConditionValue) -> Self { + value.0 + } +} +impl ::std::str::FromStr for FieldConditionValue { + 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 FieldConditionValue { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for FieldConditionValue { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for FieldConditionValue { + 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 = "`FieldConditionValuesItem`"] +#[derive(:: serde :: Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct FieldConditionValuesItem(::std::string::String); +impl ::std::ops::Deref for FieldConditionValuesItem { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: FieldConditionValuesItem) -> Self { + value.0 + } +} +impl ::std::str::FromStr for FieldConditionValuesItem { + 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 FieldConditionValuesItem { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for FieldConditionValuesItem { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for FieldConditionValuesItem { + 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 = "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)] @@ -2580,7 +2980,155 @@ pub struct FieldMigration { #[doc = "The operation's \"before\" snapshot."] pub id: Id, } -#[doc = "`FieldMigrationFromType`"] +#[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 = "RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last."] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct FieldOrder { + pub direction: FieldOrderDirection, + #[doc = "A field key (site.schema.json FieldKey's pattern, repeated)."] + pub field: FieldOrderField, +} +#[doc = "`FieldOrderDirection`"] #[derive( :: serde :: Deserialize, :: serde :: Serialize, @@ -2593,65 +3141,37 @@ pub struct FieldMigration { 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, +pub enum FieldOrderDirection { + #[serde(rename = "asc")] + Asc, + #[serde(rename = "desc")] + Desc, } -impl ::std::fmt::Display for FieldMigrationFromType { +impl ::std::fmt::Display for FieldOrderDirection { 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"), + Self::Asc => f.write_str("asc"), + Self::Desc => f.write_str("desc"), } } } -impl ::std::str::FromStr for FieldMigrationFromType { +impl ::std::str::FromStr for FieldOrderDirection { 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), + "asc" => Ok(Self::Asc), + "desc" => Ok(Self::Desc), _ => Err("invalid value".into()), } } } -impl ::std::convert::TryFrom<&str> for FieldMigrationFromType { +impl ::std::convert::TryFrom<&str> for FieldOrderDirection { type Error = self::error::ConversionError; fn try_from(value: &str) -> ::std::result::Result { value.parse() } } -impl ::std::convert::TryFrom<::std::string::String> for FieldMigrationFromType { +impl ::std::convert::TryFrom<::std::string::String> for FieldOrderDirection { type Error = self::error::ConversionError; fn try_from( value: ::std::string::String, @@ -2659,48 +3179,41 @@ impl ::std::convert::TryFrom<::std::string::String> for FieldMigrationFromType { 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`"] +#[doc = "A field key (site.schema.json FieldKey's pattern, repeated)."] #[derive(:: serde :: Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] #[serde(transparent)] -pub struct FieldOptionLabelsValue(::std::string::String); -impl ::std::ops::Deref for FieldOptionLabelsValue { +pub struct FieldOrderField(::std::string::String); +impl ::std::ops::Deref for FieldOrderField { 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 { +impl ::std::convert::From for ::std::string::String { + fn from(value: FieldOrderField) -> Self { value.0 } } -impl ::std::str::FromStr for FieldOptionLabelsValue { +impl ::std::str::FromStr for FieldOrderField { 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()); + 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 FieldOptionLabelsValue { +impl ::std::convert::TryFrom<&str> for FieldOrderField { type Error = self::error::ConversionError; fn try_from(value: &str) -> ::std::result::Result { value.parse() } } -impl ::std::convert::TryFrom<::std::string::String> for FieldOptionLabelsValue { +impl ::std::convert::TryFrom<::std::string::String> for FieldOrderField { type Error = self::error::ConversionError; fn try_from( value: ::std::string::String, @@ -2708,7 +3221,7 @@ impl ::std::convert::TryFrom<::std::string::String> for FieldOptionLabelsValue { value.parse() } } -impl<'de> ::serde::Deserialize<'de> for FieldOptionLabelsValue { +impl<'de> ::serde::Deserialize<'de> for FieldOrderField { fn deserialize(deserializer: D) -> ::std::result::Result where D: ::serde::Deserializer<'de>, @@ -3862,7 +4375,19 @@ pub enum Section { filter: ::std::option::Option, id: Id, limit: ::std::num::NonZeroU64, + #[serde( + rename = "orderBy", + skip_serializing_if = "::std::option::Option::is_none" + )] + order_by: ::std::option::Option, sort: SectionSort, + #[doc = "RFC-0042: conditions on the collection's list fields, ANDed."] + #[serde( + rename = "where", + default, + skip_serializing_if = "::std::vec::Vec::is_empty" + )] + where_: ::std::vec::Vec, }, #[doc = "ComponentSection\n\nA 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."] #[serde(rename = "component")] @@ -4464,6 +4989,61 @@ impl<'de> ::serde::Deserialize<'de> for SiteName { }) } } +#[doc = "RFC-0042: site properties a structured-data property may read."] +#[derive( + :: serde :: Deserialize, + :: serde :: Serialize, + Clone, + Copy, + Debug, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, +)] +pub enum SiteValueProperty { + #[serde(rename = "name")] + Name, + #[serde(rename = "url")] + Url, + #[serde(rename = "logo")] + Logo, +} +impl ::std::fmt::Display for SiteValueProperty { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match *self { + Self::Name => f.write_str("name"), + Self::Url => f.write_str("url"), + Self::Logo => f.write_str("logo"), + } + } +} +impl ::std::str::FromStr for SiteValueProperty { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> ::std::result::Result { + match value { + "name" => Ok(Self::Name), + "url" => Ok(Self::Url), + "logo" => Ok(Self::Logo), + _ => Err("invalid value".into()), + } + } +} +impl ::std::convert::TryFrom<&str> for SiteValueProperty { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for SiteValueProperty { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} #[doc = "Lower-case URL segment."] #[derive(:: serde :: Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] #[serde(transparent)] @@ -4675,6 +5255,203 @@ impl ::std::convert::TryFrom<::std::string::String> for SocialLinkNetwork { value.parse() } } +#[doc = "RFC-0042: the JSON-LD type an entry page emits in place of BlogPosting, with its properties mapped to value sources. Paths, fitting field types and required properties are a closed table in code (@cloudhal/core-ts/structured-data)."] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct StructuredDataPreset { + #[doc = "Keyed by property path, e.g. startDate or location.address.addressLocality."] + pub properties: + ::std::collections::HashMap, + #[serde(rename = "type")] + pub type_: StructuredDataPresetType, +} +#[doc = "`StructuredDataPresetPropertiesKey`"] +#[derive(:: serde :: Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct StructuredDataPresetPropertiesKey(::std::string::String); +impl ::std::ops::Deref for StructuredDataPresetPropertiesKey { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: StructuredDataPresetPropertiesKey) -> Self { + value.0 + } +} +impl ::std::str::FromStr for StructuredDataPresetPropertiesKey { + 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-z]{0,39}(\\.[a-z][A-Za-z]{0,39}){0,2}$").unwrap() + }); + if PATTERN.find(value).is_none() { + return Err( + "doesn't match pattern \"^[a-z][A-Za-z]{0,39}(\\.[a-z][A-Za-z]{0,39}){0,2}$\"" + .into(), + ); + } + Ok(Self(value.to_string())) + } +} +impl ::std::convert::TryFrom<&str> for StructuredDataPresetPropertiesKey { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for StructuredDataPresetPropertiesKey { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for StructuredDataPresetPropertiesKey { + 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 = "`StructuredDataPresetType`"] +#[derive( + :: serde :: Deserialize, + :: serde :: Serialize, + Clone, + Copy, + Debug, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, +)] +pub enum StructuredDataPresetType { + Event, + JobPosting, + Product, + Recipe, +} +impl ::std::fmt::Display for StructuredDataPresetType { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match *self { + Self::Event => f.write_str("Event"), + Self::JobPosting => f.write_str("JobPosting"), + Self::Product => f.write_str("Product"), + Self::Recipe => f.write_str("Recipe"), + } + } +} +impl ::std::str::FromStr for StructuredDataPresetType { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> ::std::result::Result { + match value { + "Event" => Ok(Self::Event), + "JobPosting" => Ok(Self::JobPosting), + "Product" => Ok(Self::Product), + "Recipe" => Ok(Self::Recipe), + _ => Err("invalid value".into()), + } + } +} +impl ::std::convert::TryFrom<&str> for StructuredDataPresetType { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for StructuredDataPresetType { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +#[doc = "RFC-0042: where one JSON-LD property's value comes from, discriminated by `kind`."] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, PartialEq)] +#[serde(tag = "kind", deny_unknown_fields)] +pub enum StructuredDataSource { + #[doc = "FieldValueSource\n\nA typed field of this collection, converted by its type."] + #[serde(rename = "field")] + Field { field: FieldKey }, + #[doc = "EntryValueSource\n\nA built-in property of the entry."] + #[serde(rename = "entry")] + Entry { property: EntryValueProperty }, + #[doc = "SiteValueSource\n\nA property of the site."] + #[serde(rename = "site")] + Site { property: SiteValueProperty }, + #[doc = "TextValueSource\n\nA plain-text constant: no control characters, no markup meaning; vocabulary-checked on enum paths (in code)."] + #[serde(rename = "text")] + Text { value: StructuredDataSourceValue }, +} +#[doc = "`StructuredDataSourceValue`"] +#[derive(:: serde :: Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct StructuredDataSourceValue(::std::string::String); +impl ::std::ops::Deref for StructuredDataSourceValue { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: StructuredDataSourceValue) -> Self { + value.0 + } +} +impl ::std::str::FromStr for StructuredDataSourceValue { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> ::std::result::Result { + if value.chars().count() > 200usize { + return Err("longer than 200 characters".into()); + } + if value.chars().count() < 1usize { + return Err("shorter than 1 characters".into()); + } + static PATTERN: ::std::sync::LazyLock<::regress::Regex> = + ::std::sync::LazyLock::new(|| ::regress::Regex::new("^[^\\x00-\\x1F\\x7F]+$").unwrap()); + if PATTERN.find(value).is_none() { + return Err("doesn't match pattern \"^[^\\x00-\\x1F\\x7F]+$\"".into()); + } + Ok(Self(value.to_string())) + } +} +impl ::std::convert::TryFrom<&str> for StructuredDataSourceValue { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for StructuredDataSourceValue { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for StructuredDataSourceValue { + 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 = "One element of a generative UI surface (RFC-0020 §1), in a flat list: `component` names a catalog entry (a PascalCase built-in primitive such as `Stack`, or a registry block type such as `callout`), `children` refer to other ids of the same surface, and `props` follow the entry's schema, which the store checks. Field names follow A2UI v0.9."] #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, PartialEq)] #[serde(deny_unknown_fields)] diff --git a/docs/adr/0044-collections-stage-3.md b/docs/adr/0044-collections-stage-3.md index ab5971c8..556caaed 100644 --- a/docs/adr/0044-collections-stage-3.md +++ b/docs/adr/0044-collections-stage-3.md @@ -16,7 +16,7 @@ The open stage 3 items from RFC-0040 and RFC-0041 address these: structured data 1. **Contract:** new optional collection settings. There is no `schemaVersion` bump. - `listFields`: up to 4 keys. - `expiry`: `{field, action: "unlist" | "hide", afterMinutes?}`. - - `structuredData`: `{type, properties}`. Each property names its source by `kind`: `field`, `entry`, `site` or `constant`. The entry and site property names are named `$defs` (cargo-typify merges inline enums). + - `structuredData`: `{type, properties}`. Each property names its source by `kind`: `field`, `entry`, `site` or `text` (a plain-text constant). The entry and site property names are named `$defs` (cargo-typify merges inline enums). - `contentList` sections gain `where` (up to 4 conditions) and `orderBy`. - Which properties each type accepts, the field types that fit them and the required ones are a closed table in code (`@cloudhal/core-ts/structured-data`). 2. **Presets:** Event, JobPosting, Product and Recipe. diff --git a/packages/core-contracts/fixtures/invalid/section/content-list-order-direction-up.json b/packages/core-contracts/fixtures/invalid/section/content-list-order-direction-up.json new file mode 100644 index 00000000..1e9c2e50 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/section/content-list-order-direction-up.json @@ -0,0 +1,26 @@ +{ + "reason": "direction is asc or desc", + "data": { + "id": "sec_01HZX8K3Q9WH", + "kind": "contentList", + "collection": "events", + "limit": 12, + "sort": "newest", + "where": [ + { + "field": "startsAt", + "op": "gte", + "value": "now" + }, + { + "field": "eventStatus", + "op": "in", + "values": ["scheduled"] + } + ], + "orderBy": { + "field": "startsAt", + "direction": "up" + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/section/content-list-order-extra-property.json b/packages/core-contracts/fixtures/invalid/section/content-list-order-extra-property.json new file mode 100644 index 00000000..8e369247 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/section/content-list-order-extra-property.json @@ -0,0 +1,27 @@ +{ + "reason": "an order has only field and direction", + "data": { + "id": "sec_01HZX8K3Q9WH", + "kind": "contentList", + "collection": "events", + "limit": 12, + "sort": "newest", + "where": [ + { + "field": "startsAt", + "op": "gte", + "value": "now" + }, + { + "field": "eventStatus", + "op": "in", + "values": ["scheduled"] + } + ], + "orderBy": { + "field": "startsAt", + "direction": "asc", + "nulls": "first" + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/section/content-list-order-without-direction.json b/packages/core-contracts/fixtures/invalid/section/content-list-order-without-direction.json new file mode 100644 index 00000000..bba5c557 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/section/content-list-order-without-direction.json @@ -0,0 +1,25 @@ +{ + "reason": "an order names its direction", + "data": { + "id": "sec_01HZX8K3Q9WH", + "kind": "contentList", + "collection": "events", + "limit": 12, + "sort": "newest", + "where": [ + { + "field": "startsAt", + "op": "gte", + "value": "now" + }, + { + "field": "eventStatus", + "op": "in", + "values": ["scheduled"] + } + ], + "orderBy": { + "field": "startsAt" + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/section/content-list-where-bad-field.json b/packages/core-contracts/fixtures/invalid/section/content-list-where-bad-field.json new file mode 100644 index 00000000..840fa2a9 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/section/content-list-where-bad-field.json @@ -0,0 +1,26 @@ +{ + "reason": "a condition's field is a field key", + "data": { + "id": "sec_01HZX8K3Q9WH", + "kind": "contentList", + "collection": "events", + "limit": 12, + "sort": "newest", + "where": [ + { + "field": "fields.startsAt", + "op": "gte", + "value": "now" + }, + { + "field": "eventStatus", + "op": "in", + "values": ["scheduled"] + } + ], + "orderBy": { + "field": "startsAt", + "direction": "asc" + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/section/content-list-where-empty.json b/packages/core-contracts/fixtures/invalid/section/content-list-where-empty.json new file mode 100644 index 00000000..550b772b --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/section/content-list-where-empty.json @@ -0,0 +1,15 @@ +{ + "reason": "where is absent rather than empty", + "data": { + "id": "sec_01HZX8K3Q9WH", + "kind": "contentList", + "collection": "events", + "limit": 12, + "sort": "newest", + "where": [], + "orderBy": { + "field": "startsAt", + "direction": "asc" + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/section/content-list-where-extra-property.json b/packages/core-contracts/fixtures/invalid/section/content-list-where-extra-property.json new file mode 100644 index 00000000..76ceadd5 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/section/content-list-where-extra-property.json @@ -0,0 +1,27 @@ +{ + "reason": "a condition has only field, op, value and values", + "data": { + "id": "sec_01HZX8K3Q9WH", + "kind": "contentList", + "collection": "events", + "limit": 12, + "sort": "newest", + "where": [ + { + "field": "startsAt", + "op": "gte", + "value": "now", + "offset": "7d" + }, + { + "field": "eventStatus", + "op": "in", + "values": ["scheduled"] + } + ], + "orderBy": { + "field": "startsAt", + "direction": "asc" + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/section/content-list-where-op-like.json b/packages/core-contracts/fixtures/invalid/section/content-list-where-op-like.json new file mode 100644 index 00000000..d23051bf --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/section/content-list-where-op-like.json @@ -0,0 +1,26 @@ +{ + "reason": "like is not an operator", + "data": { + "id": "sec_01HZX8K3Q9WH", + "kind": "contentList", + "collection": "events", + "limit": 12, + "sort": "newest", + "where": [ + { + "field": "startsAt", + "op": "like", + "value": "now" + }, + { + "field": "eventStatus", + "op": "in", + "values": ["scheduled"] + } + ], + "orderBy": { + "field": "startsAt", + "direction": "asc" + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/section/content-list-where-too-many.json b/packages/core-contracts/fixtures/invalid/section/content-list-where-too-many.json new file mode 100644 index 00000000..913aa033 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/section/content-list-where-too-many.json @@ -0,0 +1,64 @@ +{ + "reason": "a section has at most four conditions", + "data": { + "id": "sec_01HZX8K3Q9WH", + "kind": "contentList", + "collection": "events", + "limit": 12, + "sort": "newest", + "where": [ + { + "field": "fa", + "op": "set" + }, + { + "field": "fb", + "op": "set" + }, + { + "field": "fc", + "op": "set" + }, + { + "field": "fd", + "op": "set" + }, + { + "field": "fe", + "op": "set" + }, + { + "field": "ff", + "op": "set" + }, + { + "field": "fg", + "op": "set" + }, + { + "field": "fh", + "op": "set" + }, + { + "field": "fi", + "op": "set" + }, + { + "field": "fj", + "op": "set" + }, + { + "field": "fk", + "op": "set" + }, + { + "field": "fl", + "op": "set" + } + ], + "orderBy": { + "field": "startsAt", + "direction": "asc" + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/section/content-list-where-value-empty.json b/packages/core-contracts/fixtures/invalid/section/content-list-where-value-empty.json new file mode 100644 index 00000000..275eb49f --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/section/content-list-where-value-empty.json @@ -0,0 +1,26 @@ +{ + "reason": "a condition value is not empty", + "data": { + "id": "sec_01HZX8K3Q9WH", + "kind": "contentList", + "collection": "events", + "limit": 12, + "sort": "newest", + "where": [ + { + "field": "startsAt", + "op": "gte", + "value": "" + }, + { + "field": "eventStatus", + "op": "in", + "values": ["scheduled"] + } + ], + "orderBy": { + "field": "startsAt", + "direction": "asc" + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/section/content-list-where-value-number.json b/packages/core-contracts/fixtures/invalid/section/content-list-where-value-number.json new file mode 100644 index 00000000..d16289a3 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/section/content-list-where-value-number.json @@ -0,0 +1,26 @@ +{ + "reason": "condition values are strings, typed by the field", + "data": { + "id": "sec_01HZX8K3Q9WH", + "kind": "contentList", + "collection": "events", + "limit": 12, + "sort": "newest", + "where": [ + { + "field": "startsAt", + "op": "gte", + "value": 5 + }, + { + "field": "eventStatus", + "op": "in", + "values": ["scheduled"] + } + ], + "orderBy": { + "field": "startsAt", + "direction": "asc" + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/section/content-list-where-value-too-long.json b/packages/core-contracts/fixtures/invalid/section/content-list-where-value-too-long.json new file mode 100644 index 00000000..216647ab --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/section/content-list-where-value-too-long.json @@ -0,0 +1,26 @@ +{ + "reason": "a condition value is at most 100 characters", + "data": { + "id": "sec_01HZX8K3Q9WH", + "kind": "contentList", + "collection": "events", + "limit": 12, + "sort": "newest", + "where": [ + { + "field": "startsAt", + "op": "gte", + "value": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, + { + "field": "eventStatus", + "op": "in", + "values": ["scheduled"] + } + ], + "orderBy": { + "field": "startsAt", + "direction": "asc" + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/section/content-list-where-values-duplicate.json b/packages/core-contracts/fixtures/invalid/section/content-list-where-values-duplicate.json new file mode 100644 index 00000000..4bb17b76 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/section/content-list-where-values-duplicate.json @@ -0,0 +1,26 @@ +{ + "reason": "values are unique", + "data": { + "id": "sec_01HZX8K3Q9WH", + "kind": "contentList", + "collection": "events", + "limit": 12, + "sort": "newest", + "where": [ + { + "field": "startsAt", + "op": "gte", + "value": "now" + }, + { + "field": "eventStatus", + "op": "in", + "values": ["a", "a"] + } + ], + "orderBy": { + "field": "startsAt", + "direction": "asc" + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/section/content-list-where-values-empty.json b/packages/core-contracts/fixtures/invalid/section/content-list-where-values-empty.json new file mode 100644 index 00000000..a74594ad --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/section/content-list-where-values-empty.json @@ -0,0 +1,26 @@ +{ + "reason": "values has at least one item", + "data": { + "id": "sec_01HZX8K3Q9WH", + "kind": "contentList", + "collection": "events", + "limit": 12, + "sort": "newest", + "where": [ + { + "field": "startsAt", + "op": "gte", + "value": "now" + }, + { + "field": "eventStatus", + "op": "in", + "values": [] + } + ], + "orderBy": { + "field": "startsAt", + "direction": "asc" + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/section/content-list-where-values-too-many.json b/packages/core-contracts/fixtures/invalid/section/content-list-where-values-too-many.json new file mode 100644 index 00000000..a6c91068 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/section/content-list-where-values-too-many.json @@ -0,0 +1,48 @@ +{ + "reason": "values has at most 20 items", + "data": { + "id": "sec_01HZX8K3Q9WH", + "kind": "contentList", + "collection": "events", + "limit": 12, + "sort": "newest", + "where": [ + { + "field": "startsAt", + "op": "gte", + "value": "now" + }, + { + "field": "eventStatus", + "op": "in", + "values": [ + "v0", + "v1", + "v2", + "v3", + "v4", + "v5", + "v6", + "v7", + "v8", + "v9", + "v10", + "v11", + "v12", + "v13", + "v14", + "v15", + "v16", + "v17", + "v18", + "v19", + "v20" + ] + } + ], + "orderBy": { + "field": "startsAt", + "direction": "asc" + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/section/content-list-where-without-field.json b/packages/core-contracts/fixtures/invalid/section/content-list-where-without-field.json new file mode 100644 index 00000000..bb18a88a --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/section/content-list-where-without-field.json @@ -0,0 +1,25 @@ +{ + "reason": "a condition names its field", + "data": { + "id": "sec_01HZX8K3Q9WH", + "kind": "contentList", + "collection": "events", + "limit": 12, + "sort": "newest", + "where": [ + { + "op": "gte", + "value": "now" + }, + { + "field": "eventStatus", + "op": "in", + "values": ["scheduled"] + } + ], + "orderBy": { + "field": "startsAt", + "direction": "asc" + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/section/content-list-where-without-op.json b/packages/core-contracts/fixtures/invalid/section/content-list-where-without-op.json new file mode 100644 index 00000000..20afe035 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/section/content-list-where-without-op.json @@ -0,0 +1,25 @@ +{ + "reason": "a condition names its operator", + "data": { + "id": "sec_01HZX8K3Q9WH", + "kind": "contentList", + "collection": "events", + "limit": 12, + "sort": "newest", + "where": [ + { + "field": "startsAt", + "value": "now" + }, + { + "field": "eventStatus", + "op": "in", + "values": ["scheduled"] + } + ], + "orderBy": { + "field": "startsAt", + "direction": "asc" + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/expiry-action-delete.json b/packages/core-contracts/fixtures/invalid/site/expiry-action-delete.json new file mode 100644 index 00000000..2b8e45d6 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/expiry-action-delete.json @@ -0,0 +1,210 @@ +{ + "reason": "expiry never deletes: unlist or hide", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["startsAt", "eventStatus", "price", "day"], + "expiry": { + "field": "endsAt", + "action": "delete", + "afterMinutes": 60 + }, + "structuredData": { + "type": "Event", + "properties": { + "name": { + "kind": "entry", + "property": "title" + }, + "startDate": { + "kind": "field", + "field": "startsAt" + }, + "endDate": { + "kind": "field", + "field": "endsAt" + }, + "location.name": { + "kind": "field", + "field": "venue" + }, + "location.address.addressLocality": { + "kind": "field", + "field": "city" + }, + "location.address.addressCountry": { + "kind": "text", + "value": "TR" + }, + "eventStatus": { + "kind": "field", + "field": "eventStatus" + }, + "organizer.name": { + "kind": "site", + "property": "name" + }, + "organizer.url": { + "kind": "site", + "property": "url" + }, + "offers.price": { + "kind": "field", + "field": "price" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "TRY" + }, + "offers.url": { + "kind": "field", + "field": "tickets" + }, + "image": { + "kind": "field", + "field": "poster" + } + } + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/expiry-extra-property.json b/packages/core-contracts/fixtures/invalid/site/expiry-extra-property.json new file mode 100644 index 00000000..4bd42b6a --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/expiry-extra-property.json @@ -0,0 +1,211 @@ +{ + "reason": "an expiry has only field, action and afterMinutes", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["startsAt", "eventStatus", "price", "day"], + "expiry": { + "field": "endsAt", + "action": "unlist", + "afterMinutes": 60, + "timeZone": "UTC" + }, + "structuredData": { + "type": "Event", + "properties": { + "name": { + "kind": "entry", + "property": "title" + }, + "startDate": { + "kind": "field", + "field": "startsAt" + }, + "endDate": { + "kind": "field", + "field": "endsAt" + }, + "location.name": { + "kind": "field", + "field": "venue" + }, + "location.address.addressLocality": { + "kind": "field", + "field": "city" + }, + "location.address.addressCountry": { + "kind": "text", + "value": "TR" + }, + "eventStatus": { + "kind": "field", + "field": "eventStatus" + }, + "organizer.name": { + "kind": "site", + "property": "name" + }, + "organizer.url": { + "kind": "site", + "property": "url" + }, + "offers.price": { + "kind": "field", + "field": "price" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "TRY" + }, + "offers.url": { + "kind": "field", + "field": "tickets" + }, + "image": { + "kind": "field", + "field": "poster" + } + } + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/expiry-field-kebab.json b/packages/core-contracts/fixtures/invalid/site/expiry-field-kebab.json new file mode 100644 index 00000000..0e3711d8 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/expiry-field-kebab.json @@ -0,0 +1,210 @@ +{ + "reason": "an expiry field is a field key", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["startsAt", "eventStatus", "price", "day"], + "expiry": { + "field": "ends-at", + "action": "unlist", + "afterMinutes": 60 + }, + "structuredData": { + "type": "Event", + "properties": { + "name": { + "kind": "entry", + "property": "title" + }, + "startDate": { + "kind": "field", + "field": "startsAt" + }, + "endDate": { + "kind": "field", + "field": "endsAt" + }, + "location.name": { + "kind": "field", + "field": "venue" + }, + "location.address.addressLocality": { + "kind": "field", + "field": "city" + }, + "location.address.addressCountry": { + "kind": "text", + "value": "TR" + }, + "eventStatus": { + "kind": "field", + "field": "eventStatus" + }, + "organizer.name": { + "kind": "site", + "property": "name" + }, + "organizer.url": { + "kind": "site", + "property": "url" + }, + "offers.price": { + "kind": "field", + "field": "price" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "TRY" + }, + "offers.url": { + "kind": "field", + "field": "tickets" + }, + "image": { + "kind": "field", + "field": "poster" + } + } + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/expiry-grace-fraction.json b/packages/core-contracts/fixtures/invalid/site/expiry-grace-fraction.json new file mode 100644 index 00000000..cad79a47 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/expiry-grace-fraction.json @@ -0,0 +1,210 @@ +{ + "reason": "the grace is whole minutes", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["startsAt", "eventStatus", "price", "day"], + "expiry": { + "field": "endsAt", + "action": "unlist", + "afterMinutes": 1.5 + }, + "structuredData": { + "type": "Event", + "properties": { + "name": { + "kind": "entry", + "property": "title" + }, + "startDate": { + "kind": "field", + "field": "startsAt" + }, + "endDate": { + "kind": "field", + "field": "endsAt" + }, + "location.name": { + "kind": "field", + "field": "venue" + }, + "location.address.addressLocality": { + "kind": "field", + "field": "city" + }, + "location.address.addressCountry": { + "kind": "text", + "value": "TR" + }, + "eventStatus": { + "kind": "field", + "field": "eventStatus" + }, + "organizer.name": { + "kind": "site", + "property": "name" + }, + "organizer.url": { + "kind": "site", + "property": "url" + }, + "offers.price": { + "kind": "field", + "field": "price" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "TRY" + }, + "offers.url": { + "kind": "field", + "field": "tickets" + }, + "image": { + "kind": "field", + "field": "poster" + } + } + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/expiry-grace-negative.json b/packages/core-contracts/fixtures/invalid/site/expiry-grace-negative.json new file mode 100644 index 00000000..e810c670 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/expiry-grace-negative.json @@ -0,0 +1,210 @@ +{ + "reason": "the grace is not negative", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["startsAt", "eventStatus", "price", "day"], + "expiry": { + "field": "endsAt", + "action": "unlist", + "afterMinutes": -5 + }, + "structuredData": { + "type": "Event", + "properties": { + "name": { + "kind": "entry", + "property": "title" + }, + "startDate": { + "kind": "field", + "field": "startsAt" + }, + "endDate": { + "kind": "field", + "field": "endsAt" + }, + "location.name": { + "kind": "field", + "field": "venue" + }, + "location.address.addressLocality": { + "kind": "field", + "field": "city" + }, + "location.address.addressCountry": { + "kind": "text", + "value": "TR" + }, + "eventStatus": { + "kind": "field", + "field": "eventStatus" + }, + "organizer.name": { + "kind": "site", + "property": "name" + }, + "organizer.url": { + "kind": "site", + "property": "url" + }, + "offers.price": { + "kind": "field", + "field": "price" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "TRY" + }, + "offers.url": { + "kind": "field", + "field": "tickets" + }, + "image": { + "kind": "field", + "field": "poster" + } + } + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/expiry-grace-too-long.json b/packages/core-contracts/fixtures/invalid/site/expiry-grace-too-long.json new file mode 100644 index 00000000..5575a6ba --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/expiry-grace-too-long.json @@ -0,0 +1,210 @@ +{ + "reason": "the grace is at most a week (10080 minutes)", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["startsAt", "eventStatus", "price", "day"], + "expiry": { + "field": "endsAt", + "action": "unlist", + "afterMinutes": 20000 + }, + "structuredData": { + "type": "Event", + "properties": { + "name": { + "kind": "entry", + "property": "title" + }, + "startDate": { + "kind": "field", + "field": "startsAt" + }, + "endDate": { + "kind": "field", + "field": "endsAt" + }, + "location.name": { + "kind": "field", + "field": "venue" + }, + "location.address.addressLocality": { + "kind": "field", + "field": "city" + }, + "location.address.addressCountry": { + "kind": "text", + "value": "TR" + }, + "eventStatus": { + "kind": "field", + "field": "eventStatus" + }, + "organizer.name": { + "kind": "site", + "property": "name" + }, + "organizer.url": { + "kind": "site", + "property": "url" + }, + "offers.price": { + "kind": "field", + "field": "price" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "TRY" + }, + "offers.url": { + "kind": "field", + "field": "tickets" + }, + "image": { + "kind": "field", + "field": "poster" + } + } + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/expiry-without-action.json b/packages/core-contracts/fixtures/invalid/site/expiry-without-action.json new file mode 100644 index 00000000..481a8d32 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/expiry-without-action.json @@ -0,0 +1,209 @@ +{ + "reason": "an expiry names its action", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["startsAt", "eventStatus", "price", "day"], + "expiry": { + "field": "endsAt", + "afterMinutes": 60 + }, + "structuredData": { + "type": "Event", + "properties": { + "name": { + "kind": "entry", + "property": "title" + }, + "startDate": { + "kind": "field", + "field": "startsAt" + }, + "endDate": { + "kind": "field", + "field": "endsAt" + }, + "location.name": { + "kind": "field", + "field": "venue" + }, + "location.address.addressLocality": { + "kind": "field", + "field": "city" + }, + "location.address.addressCountry": { + "kind": "text", + "value": "TR" + }, + "eventStatus": { + "kind": "field", + "field": "eventStatus" + }, + "organizer.name": { + "kind": "site", + "property": "name" + }, + "organizer.url": { + "kind": "site", + "property": "url" + }, + "offers.price": { + "kind": "field", + "field": "price" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "TRY" + }, + "offers.url": { + "kind": "field", + "field": "tickets" + }, + "image": { + "kind": "field", + "field": "poster" + } + } + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/list-fields-bad-key.json b/packages/core-contracts/fixtures/invalid/site/list-fields-bad-key.json new file mode 100644 index 00000000..21c37f06 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/list-fields-bad-key.json @@ -0,0 +1,210 @@ +{ + "reason": "a list field is a field key", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["StartsAt"], + "expiry": { + "field": "endsAt", + "action": "unlist", + "afterMinutes": 60 + }, + "structuredData": { + "type": "Event", + "properties": { + "name": { + "kind": "entry", + "property": "title" + }, + "startDate": { + "kind": "field", + "field": "startsAt" + }, + "endDate": { + "kind": "field", + "field": "endsAt" + }, + "location.name": { + "kind": "field", + "field": "venue" + }, + "location.address.addressLocality": { + "kind": "field", + "field": "city" + }, + "location.address.addressCountry": { + "kind": "text", + "value": "TR" + }, + "eventStatus": { + "kind": "field", + "field": "eventStatus" + }, + "organizer.name": { + "kind": "site", + "property": "name" + }, + "organizer.url": { + "kind": "site", + "property": "url" + }, + "offers.price": { + "kind": "field", + "field": "price" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "TRY" + }, + "offers.url": { + "kind": "field", + "field": "tickets" + }, + "image": { + "kind": "field", + "field": "poster" + } + } + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/list-fields-duplicate.json b/packages/core-contracts/fixtures/invalid/site/list-fields-duplicate.json new file mode 100644 index 00000000..a4ba8ab5 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/list-fields-duplicate.json @@ -0,0 +1,210 @@ +{ + "reason": "list fields are unique", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["startsAt", "startsAt"], + "expiry": { + "field": "endsAt", + "action": "unlist", + "afterMinutes": 60 + }, + "structuredData": { + "type": "Event", + "properties": { + "name": { + "kind": "entry", + "property": "title" + }, + "startDate": { + "kind": "field", + "field": "startsAt" + }, + "endDate": { + "kind": "field", + "field": "endsAt" + }, + "location.name": { + "kind": "field", + "field": "venue" + }, + "location.address.addressLocality": { + "kind": "field", + "field": "city" + }, + "location.address.addressCountry": { + "kind": "text", + "value": "TR" + }, + "eventStatus": { + "kind": "field", + "field": "eventStatus" + }, + "organizer.name": { + "kind": "site", + "property": "name" + }, + "organizer.url": { + "kind": "site", + "property": "url" + }, + "offers.price": { + "kind": "field", + "field": "price" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "TRY" + }, + "offers.url": { + "kind": "field", + "field": "tickets" + }, + "image": { + "kind": "field", + "field": "poster" + } + } + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/list-fields-empty.json b/packages/core-contracts/fixtures/invalid/site/list-fields-empty.json new file mode 100644 index 00000000..b0716fcc --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/list-fields-empty.json @@ -0,0 +1,210 @@ +{ + "reason": "listFields is absent rather than empty", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": [], + "expiry": { + "field": "endsAt", + "action": "unlist", + "afterMinutes": 60 + }, + "structuredData": { + "type": "Event", + "properties": { + "name": { + "kind": "entry", + "property": "title" + }, + "startDate": { + "kind": "field", + "field": "startsAt" + }, + "endDate": { + "kind": "field", + "field": "endsAt" + }, + "location.name": { + "kind": "field", + "field": "venue" + }, + "location.address.addressLocality": { + "kind": "field", + "field": "city" + }, + "location.address.addressCountry": { + "kind": "text", + "value": "TR" + }, + "eventStatus": { + "kind": "field", + "field": "eventStatus" + }, + "organizer.name": { + "kind": "site", + "property": "name" + }, + "organizer.url": { + "kind": "site", + "property": "url" + }, + "offers.price": { + "kind": "field", + "field": "price" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "TRY" + }, + "offers.url": { + "kind": "field", + "field": "tickets" + }, + "image": { + "kind": "field", + "field": "poster" + } + } + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/list-fields-too-many.json b/packages/core-contracts/fixtures/invalid/site/list-fields-too-many.json new file mode 100644 index 00000000..8db4ced5 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/list-fields-too-many.json @@ -0,0 +1,210 @@ +{ + "reason": "a collection lists by at most four fields", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["startsAt", "eventStatus", "price", "day", "venue"], + "expiry": { + "field": "endsAt", + "action": "unlist", + "afterMinutes": 60 + }, + "structuredData": { + "type": "Event", + "properties": { + "name": { + "kind": "entry", + "property": "title" + }, + "startDate": { + "kind": "field", + "field": "startsAt" + }, + "endDate": { + "kind": "field", + "field": "endsAt" + }, + "location.name": { + "kind": "field", + "field": "venue" + }, + "location.address.addressLocality": { + "kind": "field", + "field": "city" + }, + "location.address.addressCountry": { + "kind": "text", + "value": "TR" + }, + "eventStatus": { + "kind": "field", + "field": "eventStatus" + }, + "organizer.name": { + "kind": "site", + "property": "name" + }, + "organizer.url": { + "kind": "site", + "property": "url" + }, + "offers.price": { + "kind": "field", + "field": "price" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "TRY" + }, + "offers.url": { + "kind": "field", + "field": "tickets" + }, + "image": { + "kind": "field", + "field": "poster" + } + } + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-empty-properties.json b/packages/core-contracts/fixtures/invalid/site/structured-data-empty-properties.json new file mode 100644 index 00000000..05a8c9da --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-empty-properties.json @@ -0,0 +1,157 @@ +{ + "reason": "a preset maps at least one property", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["startsAt", "eventStatus", "price", "day"], + "expiry": { + "field": "endsAt", + "action": "unlist", + "afterMinutes": 60 + }, + "structuredData": { + "type": "Event", + "properties": {} + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-entry-unknown-property.json b/packages/core-contracts/fixtures/invalid/site/structured-data-entry-unknown-property.json new file mode 100644 index 00000000..1c3a5951 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-entry-unknown-property.json @@ -0,0 +1,210 @@ +{ + "reason": "an entry source reads a closed set of properties", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["startsAt", "eventStatus", "price", "day"], + "expiry": { + "field": "endsAt", + "action": "unlist", + "afterMinutes": 60 + }, + "structuredData": { + "type": "Event", + "properties": { + "name": { + "kind": "entry", + "property": "slug" + }, + "startDate": { + "kind": "field", + "field": "startsAt" + }, + "endDate": { + "kind": "field", + "field": "endsAt" + }, + "location.name": { + "kind": "field", + "field": "venue" + }, + "location.address.addressLocality": { + "kind": "field", + "field": "city" + }, + "location.address.addressCountry": { + "kind": "text", + "value": "TR" + }, + "eventStatus": { + "kind": "field", + "field": "eventStatus" + }, + "organizer.name": { + "kind": "site", + "property": "name" + }, + "organizer.url": { + "kind": "site", + "property": "url" + }, + "offers.price": { + "kind": "field", + "field": "price" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "TRY" + }, + "offers.url": { + "kind": "field", + "field": "tickets" + }, + "image": { + "kind": "field", + "field": "poster" + } + } + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-extra-property.json b/packages/core-contracts/fixtures/invalid/site/structured-data-extra-property.json new file mode 100644 index 00000000..cf4fcd5c --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-extra-property.json @@ -0,0 +1,211 @@ +{ + "reason": "a preset has only type and properties", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["startsAt", "eventStatus", "price", "day"], + "expiry": { + "field": "endsAt", + "action": "unlist", + "afterMinutes": 60 + }, + "structuredData": { + "type": "Event", + "properties": { + "name": { + "kind": "entry", + "property": "title" + }, + "startDate": { + "kind": "field", + "field": "startsAt" + }, + "endDate": { + "kind": "field", + "field": "endsAt" + }, + "location.name": { + "kind": "field", + "field": "venue" + }, + "location.address.addressLocality": { + "kind": "field", + "field": "city" + }, + "location.address.addressCountry": { + "kind": "text", + "value": "TR" + }, + "eventStatus": { + "kind": "field", + "field": "eventStatus" + }, + "organizer.name": { + "kind": "site", + "property": "name" + }, + "organizer.url": { + "kind": "site", + "property": "url" + }, + "offers.price": { + "kind": "field", + "field": "price" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "TRY" + }, + "offers.url": { + "kind": "field", + "field": "tickets" + }, + "image": { + "kind": "field", + "field": "poster" + } + }, + "context": "https://schema.org" + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-faq-page.json b/packages/core-contracts/fixtures/invalid/site/structured-data-faq-page.json new file mode 100644 index 00000000..ede104da --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-faq-page.json @@ -0,0 +1,210 @@ +{ + "reason": "FAQPage is not an offered preset type", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["startsAt", "eventStatus", "price", "day"], + "expiry": { + "field": "endsAt", + "action": "unlist", + "afterMinutes": 60 + }, + "structuredData": { + "type": "FAQPage", + "properties": { + "name": { + "kind": "entry", + "property": "title" + }, + "startDate": { + "kind": "field", + "field": "startsAt" + }, + "endDate": { + "kind": "field", + "field": "endsAt" + }, + "location.name": { + "kind": "field", + "field": "venue" + }, + "location.address.addressLocality": { + "kind": "field", + "field": "city" + }, + "location.address.addressCountry": { + "kind": "text", + "value": "TR" + }, + "eventStatus": { + "kind": "field", + "field": "eventStatus" + }, + "organizer.name": { + "kind": "site", + "property": "name" + }, + "organizer.url": { + "kind": "site", + "property": "url" + }, + "offers.price": { + "kind": "field", + "field": "price" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "TRY" + }, + "offers.url": { + "kind": "field", + "field": "tickets" + }, + "image": { + "kind": "field", + "field": "poster" + } + } + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-field-missing.json b/packages/core-contracts/fixtures/invalid/site/structured-data-field-missing.json new file mode 100644 index 00000000..5ef6d9ab --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-field-missing.json @@ -0,0 +1,209 @@ +{ + "reason": "a field source names its field", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["startsAt", "eventStatus", "price", "day"], + "expiry": { + "field": "endsAt", + "action": "unlist", + "afterMinutes": 60 + }, + "structuredData": { + "type": "Event", + "properties": { + "name": { + "kind": "entry", + "property": "title" + }, + "startDate": { + "kind": "field" + }, + "endDate": { + "kind": "field", + "field": "endsAt" + }, + "location.name": { + "kind": "field", + "field": "venue" + }, + "location.address.addressLocality": { + "kind": "field", + "field": "city" + }, + "location.address.addressCountry": { + "kind": "text", + "value": "TR" + }, + "eventStatus": { + "kind": "field", + "field": "eventStatus" + }, + "organizer.name": { + "kind": "site", + "property": "name" + }, + "organizer.url": { + "kind": "site", + "property": "url" + }, + "offers.price": { + "kind": "field", + "field": "price" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "TRY" + }, + "offers.url": { + "kind": "field", + "field": "tickets" + }, + "image": { + "kind": "field", + "field": "poster" + } + } + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-field-with-value.json b/packages/core-contracts/fixtures/invalid/site/structured-data-field-with-value.json new file mode 100644 index 00000000..3740f3e1 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-field-with-value.json @@ -0,0 +1,211 @@ +{ + "reason": "a field source has no value", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["startsAt", "eventStatus", "price", "day"], + "expiry": { + "field": "endsAt", + "action": "unlist", + "afterMinutes": 60 + }, + "structuredData": { + "type": "Event", + "properties": { + "name": { + "kind": "entry", + "property": "title" + }, + "startDate": { + "kind": "field", + "field": "startsAt", + "value": "x" + }, + "endDate": { + "kind": "field", + "field": "endsAt" + }, + "location.name": { + "kind": "field", + "field": "venue" + }, + "location.address.addressLocality": { + "kind": "field", + "field": "city" + }, + "location.address.addressCountry": { + "kind": "text", + "value": "TR" + }, + "eventStatus": { + "kind": "field", + "field": "eventStatus" + }, + "organizer.name": { + "kind": "site", + "property": "name" + }, + "organizer.url": { + "kind": "site", + "property": "url" + }, + "offers.price": { + "kind": "field", + "field": "price" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "TRY" + }, + "offers.url": { + "kind": "field", + "field": "tickets" + }, + "image": { + "kind": "field", + "field": "poster" + } + } + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-kind-html.json b/packages/core-contracts/fixtures/invalid/site/structured-data-kind-html.json new file mode 100644 index 00000000..989c44f2 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-kind-html.json @@ -0,0 +1,214 @@ +{ + "reason": "html is not a source kind", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["startsAt", "eventStatus", "price", "day"], + "expiry": { + "field": "endsAt", + "action": "unlist", + "afterMinutes": 60 + }, + "structuredData": { + "type": "Event", + "properties": { + "name": { + "kind": "entry", + "property": "title" + }, + "startDate": { + "kind": "field", + "field": "startsAt" + }, + "endDate": { + "kind": "field", + "field": "endsAt" + }, + "location.name": { + "kind": "field", + "field": "venue" + }, + "location.address.addressLocality": { + "kind": "field", + "field": "city" + }, + "location.address.addressCountry": { + "kind": "text", + "value": "TR" + }, + "eventStatus": { + "kind": "field", + "field": "eventStatus" + }, + "organizer.name": { + "kind": "site", + "property": "name" + }, + "organizer.url": { + "kind": "site", + "property": "url" + }, + "offers.price": { + "kind": "field", + "field": "price" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "TRY" + }, + "offers.url": { + "kind": "field", + "field": "tickets" + }, + "image": { + "kind": "field", + "field": "poster" + }, + "description": { + "kind": "html", + "value": "x" + } + } + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-path-digit.json b/packages/core-contracts/fixtures/invalid/site/structured-data-path-digit.json new file mode 100644 index 00000000..d615dcf3 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-path-digit.json @@ -0,0 +1,214 @@ +{ + "reason": "a property path segment is letters only", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["startsAt", "eventStatus", "price", "day"], + "expiry": { + "field": "endsAt", + "action": "unlist", + "afterMinutes": 60 + }, + "structuredData": { + "type": "Event", + "properties": { + "name": { + "kind": "entry", + "property": "title" + }, + "startDate": { + "kind": "field", + "field": "startsAt" + }, + "endDate": { + "kind": "field", + "field": "endsAt" + }, + "location.name": { + "kind": "field", + "field": "venue" + }, + "location.address.addressLocality": { + "kind": "field", + "field": "city" + }, + "location.address.addressCountry": { + "kind": "text", + "value": "TR" + }, + "eventStatus": { + "kind": "field", + "field": "eventStatus" + }, + "organizer.name": { + "kind": "site", + "property": "name" + }, + "organizer.url": { + "kind": "site", + "property": "url" + }, + "offers.price": { + "kind": "field", + "field": "price" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "TRY" + }, + "offers.url": { + "kind": "field", + "field": "tickets" + }, + "image": { + "kind": "field", + "field": "poster" + }, + "offers2.price": { + "kind": "text", + "value": "1" + } + } + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-path-too-deep.json b/packages/core-contracts/fixtures/invalid/site/structured-data-path-too-deep.json new file mode 100644 index 00000000..697b09a5 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-path-too-deep.json @@ -0,0 +1,214 @@ +{ + "reason": "a property path has at most three segments", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["startsAt", "eventStatus", "price", "day"], + "expiry": { + "field": "endsAt", + "action": "unlist", + "afterMinutes": 60 + }, + "structuredData": { + "type": "Event", + "properties": { + "name": { + "kind": "entry", + "property": "title" + }, + "startDate": { + "kind": "field", + "field": "startsAt" + }, + "endDate": { + "kind": "field", + "field": "endsAt" + }, + "location.name": { + "kind": "field", + "field": "venue" + }, + "location.address.addressLocality": { + "kind": "field", + "field": "city" + }, + "location.address.addressCountry": { + "kind": "text", + "value": "TR" + }, + "eventStatus": { + "kind": "field", + "field": "eventStatus" + }, + "organizer.name": { + "kind": "site", + "property": "name" + }, + "organizer.url": { + "kind": "site", + "property": "url" + }, + "offers.price": { + "kind": "field", + "field": "price" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "TRY" + }, + "offers.url": { + "kind": "field", + "field": "tickets" + }, + "image": { + "kind": "field", + "field": "poster" + }, + "location.address.geo.latitude": { + "kind": "text", + "value": "1" + } + } + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-path-upper.json b/packages/core-contracts/fixtures/invalid/site/structured-data-path-upper.json new file mode 100644 index 00000000..8c9eeec1 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-path-upper.json @@ -0,0 +1,214 @@ +{ + "reason": "a property path starts lower case", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["startsAt", "eventStatus", "price", "day"], + "expiry": { + "field": "endsAt", + "action": "unlist", + "afterMinutes": 60 + }, + "structuredData": { + "type": "Event", + "properties": { + "name": { + "kind": "entry", + "property": "title" + }, + "startDate": { + "kind": "field", + "field": "startsAt" + }, + "endDate": { + "kind": "field", + "field": "endsAt" + }, + "location.name": { + "kind": "field", + "field": "venue" + }, + "location.address.addressLocality": { + "kind": "field", + "field": "city" + }, + "location.address.addressCountry": { + "kind": "text", + "value": "TR" + }, + "eventStatus": { + "kind": "field", + "field": "eventStatus" + }, + "organizer.name": { + "kind": "site", + "property": "name" + }, + "organizer.url": { + "kind": "site", + "property": "url" + }, + "offers.price": { + "kind": "field", + "field": "price" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "TRY" + }, + "offers.url": { + "kind": "field", + "field": "tickets" + }, + "image": { + "kind": "field", + "field": "poster" + }, + "Location": { + "kind": "text", + "value": "x" + } + } + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-site-entry-property.json b/packages/core-contracts/fixtures/invalid/site/structured-data-site-entry-property.json new file mode 100644 index 00000000..5ee76a11 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-site-entry-property.json @@ -0,0 +1,210 @@ +{ + "reason": "a site source cannot read an entry property", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["startsAt", "eventStatus", "price", "day"], + "expiry": { + "field": "endsAt", + "action": "unlist", + "afterMinutes": 60 + }, + "structuredData": { + "type": "Event", + "properties": { + "name": { + "kind": "entry", + "property": "title" + }, + "startDate": { + "kind": "field", + "field": "startsAt" + }, + "endDate": { + "kind": "field", + "field": "endsAt" + }, + "location.name": { + "kind": "field", + "field": "venue" + }, + "location.address.addressLocality": { + "kind": "field", + "field": "city" + }, + "location.address.addressCountry": { + "kind": "text", + "value": "TR" + }, + "eventStatus": { + "kind": "field", + "field": "eventStatus" + }, + "organizer.name": { + "kind": "site", + "property": "title" + }, + "organizer.url": { + "kind": "site", + "property": "url" + }, + "offers.price": { + "kind": "field", + "field": "price" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "TRY" + }, + "offers.url": { + "kind": "field", + "field": "tickets" + }, + "image": { + "kind": "field", + "field": "poster" + } + } + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-site-unknown-property.json b/packages/core-contracts/fixtures/invalid/site/structured-data-site-unknown-property.json new file mode 100644 index 00000000..c058622d --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-site-unknown-property.json @@ -0,0 +1,210 @@ +{ + "reason": "a site source reads a closed set of properties", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["startsAt", "eventStatus", "price", "day"], + "expiry": { + "field": "endsAt", + "action": "unlist", + "afterMinutes": 60 + }, + "structuredData": { + "type": "Event", + "properties": { + "name": { + "kind": "entry", + "property": "title" + }, + "startDate": { + "kind": "field", + "field": "startsAt" + }, + "endDate": { + "kind": "field", + "field": "endsAt" + }, + "location.name": { + "kind": "field", + "field": "venue" + }, + "location.address.addressLocality": { + "kind": "field", + "field": "city" + }, + "location.address.addressCountry": { + "kind": "text", + "value": "TR" + }, + "eventStatus": { + "kind": "field", + "field": "eventStatus" + }, + "organizer.name": { + "kind": "site", + "property": "domain" + }, + "organizer.url": { + "kind": "site", + "property": "url" + }, + "offers.price": { + "kind": "field", + "field": "price" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "TRY" + }, + "offers.url": { + "kind": "field", + "field": "tickets" + }, + "image": { + "kind": "field", + "field": "poster" + } + } + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-text-control-char.json b/packages/core-contracts/fixtures/invalid/site/structured-data-text-control-char.json new file mode 100644 index 00000000..b48426d4 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-text-control-char.json @@ -0,0 +1,214 @@ +{ + "reason": "a text constant has no control characters", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["startsAt", "eventStatus", "price", "day"], + "expiry": { + "field": "endsAt", + "action": "unlist", + "afterMinutes": 60 + }, + "structuredData": { + "type": "Event", + "properties": { + "name": { + "kind": "entry", + "property": "title" + }, + "startDate": { + "kind": "field", + "field": "startsAt" + }, + "endDate": { + "kind": "field", + "field": "endsAt" + }, + "location.name": { + "kind": "field", + "field": "venue" + }, + "location.address.addressLocality": { + "kind": "field", + "field": "city" + }, + "location.address.addressCountry": { + "kind": "text", + "value": "TR" + }, + "eventStatus": { + "kind": "field", + "field": "eventStatus" + }, + "organizer.name": { + "kind": "site", + "property": "name" + }, + "organizer.url": { + "kind": "site", + "property": "url" + }, + "offers.price": { + "kind": "field", + "field": "price" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "TRY" + }, + "offers.url": { + "kind": "field", + "field": "tickets" + }, + "image": { + "kind": "field", + "field": "poster" + }, + "description": { + "kind": "text", + "value": "line\nbreak" + } + } + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-text-empty.json b/packages/core-contracts/fixtures/invalid/site/structured-data-text-empty.json new file mode 100644 index 00000000..1ac1d185 --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-text-empty.json @@ -0,0 +1,214 @@ +{ + "reason": "a text constant is not empty", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["startsAt", "eventStatus", "price", "day"], + "expiry": { + "field": "endsAt", + "action": "unlist", + "afterMinutes": 60 + }, + "structuredData": { + "type": "Event", + "properties": { + "name": { + "kind": "entry", + "property": "title" + }, + "startDate": { + "kind": "field", + "field": "startsAt" + }, + "endDate": { + "kind": "field", + "field": "endsAt" + }, + "location.name": { + "kind": "field", + "field": "venue" + }, + "location.address.addressLocality": { + "kind": "field", + "field": "city" + }, + "location.address.addressCountry": { + "kind": "text", + "value": "TR" + }, + "eventStatus": { + "kind": "field", + "field": "eventStatus" + }, + "organizer.name": { + "kind": "site", + "property": "name" + }, + "organizer.url": { + "kind": "site", + "property": "url" + }, + "offers.price": { + "kind": "field", + "field": "price" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "TRY" + }, + "offers.url": { + "kind": "field", + "field": "tickets" + }, + "image": { + "kind": "field", + "field": "poster" + }, + "description": { + "kind": "text", + "value": "" + } + } + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-text-too-long.json b/packages/core-contracts/fixtures/invalid/site/structured-data-text-too-long.json new file mode 100644 index 00000000..2e606a8b --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-text-too-long.json @@ -0,0 +1,214 @@ +{ + "reason": "a text constant is at most 200 characters", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["startsAt", "eventStatus", "price", "day"], + "expiry": { + "field": "endsAt", + "action": "unlist", + "afterMinutes": 60 + }, + "structuredData": { + "type": "Event", + "properties": { + "name": { + "kind": "entry", + "property": "title" + }, + "startDate": { + "kind": "field", + "field": "startsAt" + }, + "endDate": { + "kind": "field", + "field": "endsAt" + }, + "location.name": { + "kind": "field", + "field": "venue" + }, + "location.address.addressLocality": { + "kind": "field", + "field": "city" + }, + "location.address.addressCountry": { + "kind": "text", + "value": "TR" + }, + "eventStatus": { + "kind": "field", + "field": "eventStatus" + }, + "organizer.name": { + "kind": "site", + "property": "name" + }, + "organizer.url": { + "kind": "site", + "property": "url" + }, + "offers.price": { + "kind": "field", + "field": "price" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "TRY" + }, + "offers.url": { + "kind": "field", + "field": "tickets" + }, + "image": { + "kind": "field", + "field": "poster" + }, + "description": { + "kind": "text", + "value": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + } + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-too-many-properties.json b/packages/core-contracts/fixtures/invalid/site/structured-data-too-many-properties.json new file mode 100644 index 00000000..c11383ab --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-too-many-properties.json @@ -0,0 +1,322 @@ +{ + "reason": "a preset maps at most 40 properties", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["startsAt", "eventStatus", "price", "day"], + "expiry": { + "field": "endsAt", + "action": "unlist", + "afterMinutes": 60 + }, + "structuredData": { + "type": "Event", + "properties": { + "pa": { + "kind": "text", + "value": "x" + }, + "pb": { + "kind": "text", + "value": "x" + }, + "pc": { + "kind": "text", + "value": "x" + }, + "pd": { + "kind": "text", + "value": "x" + }, + "pe": { + "kind": "text", + "value": "x" + }, + "pf": { + "kind": "text", + "value": "x" + }, + "pg": { + "kind": "text", + "value": "x" + }, + "ph": { + "kind": "text", + "value": "x" + }, + "pi": { + "kind": "text", + "value": "x" + }, + "pj": { + "kind": "text", + "value": "x" + }, + "pk": { + "kind": "text", + "value": "x" + }, + "pl": { + "kind": "text", + "value": "x" + }, + "pm": { + "kind": "text", + "value": "x" + }, + "pn": { + "kind": "text", + "value": "x" + }, + "po": { + "kind": "text", + "value": "x" + }, + "pp": { + "kind": "text", + "value": "x" + }, + "pq": { + "kind": "text", + "value": "x" + }, + "pr": { + "kind": "text", + "value": "x" + }, + "ps": { + "kind": "text", + "value": "x" + }, + "pt": { + "kind": "text", + "value": "x" + }, + "pu": { + "kind": "text", + "value": "x" + }, + "pv": { + "kind": "text", + "value": "x" + }, + "pw": { + "kind": "text", + "value": "x" + }, + "px": { + "kind": "text", + "value": "x" + }, + "py": { + "kind": "text", + "value": "x" + }, + "pz": { + "kind": "text", + "value": "x" + }, + "pA": { + "kind": "text", + "value": "x" + }, + "pB": { + "kind": "text", + "value": "x" + }, + "pC": { + "kind": "text", + "value": "x" + }, + "pD": { + "kind": "text", + "value": "x" + }, + "pE": { + "kind": "text", + "value": "x" + }, + "pF": { + "kind": "text", + "value": "x" + }, + "pG": { + "kind": "text", + "value": "x" + }, + "pH": { + "kind": "text", + "value": "x" + }, + "pI": { + "kind": "text", + "value": "x" + }, + "pJ": { + "kind": "text", + "value": "x" + }, + "pK": { + "kind": "text", + "value": "x" + }, + "pL": { + "kind": "text", + "value": "x" + }, + "pM": { + "kind": "text", + "value": "x" + }, + "pN": { + "kind": "text", + "value": "x" + }, + "pO": { + "kind": "text", + "value": "x" + } + } + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-without-type.json b/packages/core-contracts/fixtures/invalid/site/structured-data-without-type.json new file mode 100644 index 00000000..6d70169a --- /dev/null +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-without-type.json @@ -0,0 +1,209 @@ +{ + "reason": "a preset names its type", + "data": { + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["startsAt", "eventStatus", "price", "day"], + "expiry": { + "field": "endsAt", + "action": "unlist", + "afterMinutes": 60 + }, + "structuredData": { + "properties": { + "name": { + "kind": "entry", + "property": "title" + }, + "startDate": { + "kind": "field", + "field": "startsAt" + }, + "endDate": { + "kind": "field", + "field": "endsAt" + }, + "location.name": { + "kind": "field", + "field": "venue" + }, + "location.address.addressLocality": { + "kind": "field", + "field": "city" + }, + "location.address.addressCountry": { + "kind": "text", + "value": "TR" + }, + "eventStatus": { + "kind": "field", + "field": "eventStatus" + }, + "organizer.name": { + "kind": "site", + "property": "name" + }, + "organizer.url": { + "kind": "site", + "property": "url" + }, + "offers.price": { + "kind": "field", + "field": "price" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "TRY" + }, + "offers.url": { + "kind": "field", + "field": "tickets" + }, + "image": { + "kind": "field", + "field": "poster" + } + } + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/valid/section/content-list-where-ops.json b/packages/core-contracts/fixtures/valid/section/content-list-where-ops.json new file mode 100644 index 00000000..98e8f6c8 --- /dev/null +++ b/packages/core-contracts/fixtures/valid/section/content-list-where-ops.json @@ -0,0 +1,32 @@ +{ + "id": "sec_01HZX8K3Q9WO", + "kind": "contentList", + "collection": "events", + "limit": 5, + "sort": "oldest", + "filter": "tag:music", + "where": [ + { + "field": "poster", + "op": "set" + }, + { + "field": "price", + "op": "lte", + "value": "100" + }, + { + "field": "day", + "op": "lt", + "value": "2026-12-31" + }, + { + "field": "free", + "op": "unset" + } + ], + "orderBy": { + "field": "price", + "direction": "desc" + } +} diff --git a/packages/core-contracts/fixtures/valid/section/content-list-where.json b/packages/core-contracts/fixtures/valid/section/content-list-where.json new file mode 100644 index 00000000..b67f87ef --- /dev/null +++ b/packages/core-contracts/fixtures/valid/section/content-list-where.json @@ -0,0 +1,23 @@ +{ + "id": "sec_01HZX8K3Q9WH", + "kind": "contentList", + "collection": "events", + "limit": 12, + "sort": "newest", + "where": [ + { + "field": "startsAt", + "op": "gte", + "value": "now" + }, + { + "field": "eventStatus", + "op": "in", + "values": ["scheduled"] + } + ], + "orderBy": { + "field": "startsAt", + "direction": "asc" + } +} diff --git a/packages/core-contracts/fixtures/valid/site/collection-stage3-presets.json b/packages/core-contracts/fixtures/valid/site/collection-stage3-presets.json new file mode 100644 index 00000000..1006f934 --- /dev/null +++ b/packages/core-contracts/fixtures/valid/site/collection-stage3-presets.json @@ -0,0 +1,152 @@ +{ + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "jobs": { + "fields": [ + { + "key": "closes", + "type": "date", + "labels": { + "en": { + "label": "closes" + } + }, + "localized": false + }, + { + "key": "remote", + "type": "boolean", + "labels": { + "en": { + "label": "remote" + } + }, + "localized": false + } + ], + "expiry": { + "field": "closes", + "action": "hide" + }, + "structuredData": { + "type": "JobPosting", + "properties": { + "title": { + "kind": "entry", + "property": "title" + }, + "description": { + "kind": "entry", + "property": "body" + }, + "datePosted": { + "kind": "entry", + "property": "publishedAt" + }, + "validThrough": { + "kind": "field", + "field": "closes" + }, + "hiringOrganization.name": { + "kind": "site", + "property": "name" + }, + "hiringOrganization.logo": { + "kind": "site", + "property": "logo" + }, + "jobLocationType": { + "kind": "field", + "field": "remote" + }, + "applicantLocationRequirements.name": { + "kind": "text", + "value": "Türkiye" + }, + "employmentType": { + "kind": "text", + "value": "FULL_TIME" + } + } + } + }, + "products": { + "structuredData": { + "type": "Product", + "properties": { + "offers.price": { + "kind": "field", + "field": "price" + }, + "image": { + "kind": "entry", + "property": "coverImage" + }, + "description": { + "kind": "entry", + "property": "summary" + }, + "offers.priceValidUntil": { + "kind": "entry", + "property": "updatedAt" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "EUR" + } + } + }, + "fields": [ + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + } + ] + }, + "recipes": { + "listFields": ["minutes"], + "fields": [ + { + "key": "minutes", + "type": "number", + "labels": { + "en": { + "label": "minutes" + } + }, + "localized": false, + "integer": true + } + ], + "structuredData": { + "type": "Recipe", + "properties": { + "totalTime": { + "kind": "field", + "field": "minutes" + } + } + } + } + } +} diff --git a/packages/core-contracts/fixtures/valid/site/collection-stage3.json b/packages/core-contracts/fixtures/valid/site/collection-stage3.json new file mode 100644 index 00000000..22f96580 --- /dev/null +++ b/packages/core-contracts/fixtures/valid/site/collection-stage3.json @@ -0,0 +1,207 @@ +{ + "schemaVersion": 1, + "id": "site_01HZX8K3Q9T1", + "projectId": "proj_01HZX8K3Q9AB", + "name": "Events", + "slug": "events", + "defaultLocale": "en", + "locales": ["en", "tr"], + "domains": [], + "pageIds": [], + "theme": { + "name": "blog" + }, + "timeZone": "Europe/Istanbul", + "rev": 0, + "createdAt": "2026-10-23T20:00:00Z", + "updatedAt": "2026-10-23T20:00:00Z", + "collections": { + "events": { + "labels": { + "en": { + "singular": "Event", + "plural": "Events" + } + }, + "fields": [ + { + "key": "startsAt", + "type": "datetime", + "labels": { + "en": { + "label": "startsAt" + } + }, + "localized": false + }, + { + "key": "endsAt", + "type": "datetime", + "labels": { + "en": { + "label": "endsAt" + } + }, + "localized": false + }, + { + "key": "day", + "type": "date", + "labels": { + "en": { + "label": "day" + } + }, + "localized": false + }, + { + "key": "venue", + "type": "text", + "labels": { + "en": { + "label": "venue" + } + }, + "localized": false + }, + { + "key": "city", + "type": "text", + "labels": { + "en": { + "label": "city" + } + }, + "localized": false + }, + { + "key": "price", + "type": "number", + "labels": { + "en": { + "label": "price" + } + }, + "localized": false + }, + { + "key": "eventStatus", + "type": "select", + "labels": { + "en": { + "label": "eventStatus" + } + }, + "localized": false, + "options": [ + { + "value": "scheduled", + "labels": { + "en": "Scheduled" + } + }, + { + "value": "cancelled", + "labels": { + "en": "Cancelled" + } + } + ] + }, + { + "key": "poster", + "type": "image", + "labels": { + "en": { + "label": "poster" + } + }, + "localized": false + }, + { + "key": "tickets", + "type": "url", + "labels": { + "en": { + "label": "tickets" + } + }, + "localized": false + }, + { + "key": "notes", + "type": "richText", + "labels": { + "en": { + "label": "notes" + } + }, + "localized": false + } + ], + "listFields": ["startsAt", "eventStatus", "price", "day"], + "expiry": { + "field": "endsAt", + "action": "unlist", + "afterMinutes": 60 + }, + "structuredData": { + "type": "Event", + "properties": { + "name": { + "kind": "entry", + "property": "title" + }, + "startDate": { + "kind": "field", + "field": "startsAt" + }, + "endDate": { + "kind": "field", + "field": "endsAt" + }, + "location.name": { + "kind": "field", + "field": "venue" + }, + "location.address.addressLocality": { + "kind": "field", + "field": "city" + }, + "location.address.addressCountry": { + "kind": "text", + "value": "TR" + }, + "eventStatus": { + "kind": "field", + "field": "eventStatus" + }, + "organizer.name": { + "kind": "site", + "property": "name" + }, + "organizer.url": { + "kind": "site", + "property": "url" + }, + "offers.price": { + "kind": "field", + "field": "price" + }, + "offers.priceCurrency": { + "kind": "text", + "value": "TRY" + }, + "offers.url": { + "kind": "field", + "field": "tickets" + }, + "image": { + "kind": "field", + "field": "poster" + } + } + } + } + } +} diff --git a/packages/core-contracts/schemas/common.schema.json b/packages/core-contracts/schemas/common.schema.json index dc289f7c..a45ecf6a 100644 --- a/packages/core-contracts/schemas/common.schema.json +++ b/packages/core-contracts/schemas/common.schema.json @@ -177,6 +177,56 @@ } } }, + "FieldCondition": { + "type": "object", + "description": "RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`.", + "additionalProperties": false, + "required": ["field", "op"], + "properties": { + "field": { + "type": "string", + "description": "A field key (site.schema.json FieldKey's pattern, repeated).", + "pattern": "^[a-z][A-Za-z0-9]{0,39}$" + }, + "op": { + "type": "string", + "enum": ["eq", "ne", "lt", "lte", "gt", "gte", "in", "set", "unset"] + }, + "value": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "values": { + "type": "array", + "minItems": 1, + "maxItems": 20, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 100 + } + } + } + }, + "FieldOrder": { + "type": "object", + "description": "RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last.", + "additionalProperties": false, + "required": ["field", "direction"], + "properties": { + "field": { + "type": "string", + "description": "A field key (site.schema.json FieldKey's pattern, repeated).", + "pattern": "^[a-z][A-Za-z0-9]{0,39}$" + }, + "direction": { + "type": "string", + "enum": ["asc", "desc"] + } + } + }, "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.", diff --git a/packages/core-contracts/schemas/section.schema.json b/packages/core-contracts/schemas/section.schema.json index 9bf0494f..25d90570 100644 --- a/packages/core-contracts/schemas/section.schema.json +++ b/packages/core-contracts/schemas/section.schema.json @@ -61,7 +61,15 @@ "description": "Filter expression over term assignments (RFC-0002), e.g. 'category:news+tag:ai' or 'category:news>*'.", "minLength": 1, "maxLength": 512 - } + }, + "where": { + "type": "array", + "description": "RFC-0042: conditions on the collection's list fields, ANDed.", + "minItems": 1, + "maxItems": 4, + "items": { "$ref": "common.schema.json#/$defs/FieldCondition" } + }, + "orderBy": { "$ref": "common.schema.json#/$defs/FieldOrder" } } }, { diff --git a/packages/core-contracts/schemas/site.schema.json b/packages/core-contracts/schemas/site.schema.json index 2eaafb58..79256312 100644 --- a/packages/core-contracts/schemas/site.schema.json +++ b/packages/core-contracts/schemas/site.schema.json @@ -113,9 +113,154 @@ "items": { "$ref": "#/$defs/FieldDefinition" } + }, + "listFields": { + "type": "array", + "description": "RFC-0042: fields that listings, queries, Studio columns and MCP may filter and sort by, in column order. Keys of this collection's fields (checked in code).", + "minItems": 1, + "maxItems": 4, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/FieldKey" + } + }, + "expiry": { + "$ref": "#/$defs/CollectionExpiry" + }, + "structuredData": { + "$ref": "#/$defs/StructuredDataPreset" + } + } + }, + "CollectionExpiry": { + "type": "object", + "description": "RFC-0042: a time gate every entry derives from one of its date or datetime fields, checked at render time like Availability.until. A date ends at the start of the next day in the site's time zone.", + "additionalProperties": false, + "required": ["field", "action"], + "properties": { + "field": { + "$ref": "#/$defs/FieldKey" + }, + "action": { + "type": "string", + "description": "unlist: the page stays, the entry leaves listings, feeds, the sitemap and search. hide: every public route answers 404.", + "enum": ["unlist", "hide"] + }, + "afterMinutes": { + "type": "integer", + "description": "Grace after the field's instant. Absent: 0.", + "minimum": 0, + "maximum": 10080 + } + } + }, + "StructuredDataPreset": { + "type": "object", + "description": "RFC-0042: the JSON-LD type an entry page emits in place of BlogPosting, with its properties mapped to value sources. Paths, fitting field types and required properties are a closed table in code (@cloudhal/core-ts/structured-data).", + "additionalProperties": false, + "required": ["type", "properties"], + "properties": { + "type": { + "type": "string", + "enum": ["Event", "JobPosting", "Product", "Recipe"] + }, + "properties": { + "type": "object", + "description": "Keyed by property path, e.g. startDate or location.address.addressLocality.", + "propertyNames": { + "type": "string", + "pattern": "^[a-z][A-Za-z]{0,39}(\\.[a-z][A-Za-z]{0,39}){0,2}$" + }, + "minProperties": 1, + "maxProperties": 40, + "additionalProperties": { + "$ref": "#/$defs/StructuredDataSource" + } } } }, + "StructuredDataSource": { + "description": "RFC-0042: where one JSON-LD property's value comes from, discriminated by `kind`.", + "oneOf": [ + { + "title": "FieldValueSource", + "type": "object", + "description": "A typed field of this collection, converted by its type.", + "additionalProperties": false, + "required": ["kind", "field"], + "properties": { + "kind": { + "type": "string", + "enum": ["field"] + }, + "field": { + "$ref": "#/$defs/FieldKey" + } + } + }, + { + "title": "EntryValueSource", + "type": "object", + "description": "A built-in property of the entry.", + "additionalProperties": false, + "required": ["kind", "property"], + "properties": { + "kind": { + "type": "string", + "enum": ["entry"] + }, + "property": { + "$ref": "#/$defs/EntryValueProperty" + } + } + }, + { + "title": "SiteValueSource", + "type": "object", + "description": "A property of the site.", + "additionalProperties": false, + "required": ["kind", "property"], + "properties": { + "kind": { + "type": "string", + "enum": ["site"] + }, + "property": { + "$ref": "#/$defs/SiteValueProperty" + } + } + }, + { + "title": "TextValueSource", + "type": "object", + "description": "A plain-text constant: no control characters, no markup meaning; vocabulary-checked on enum paths (in code).", + "additionalProperties": false, + "required": ["kind", "value"], + "properties": { + "kind": { + "type": "string", + "enum": ["text"] + }, + "value": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "pattern": "^[^\\x00-\\x1F\\x7F]+$" + } + } + } + ] + }, + "EntryValueProperty": { + "type": "string", + "description": "RFC-0042: entry properties a structured-data property may read.", + "enum": ["title", "summary", "body", "coverImage", "url", "publishedAt", "updatedAt"] + }, + "SiteValueProperty": { + "type": "string", + "description": "RFC-0042: site properties a structured-data property may read.", + "enum": ["name", "url", "logo"] + }, "CollectionLabel": { "type": "object", "additionalProperties": false, diff --git a/packages/core-contracts/src/generated/index.ts b/packages/core-contracts/src/generated/index.ts index d6f81151..5926f38d 100644 --- a/packages/core-contracts/src/generated/index.ts +++ b/packages/core-contracts/src/generated/index.ts @@ -6,7 +6,7 @@ */ export type { SchemaVersion, Id, Slug, Revision, Timestamp, Project, Metadata } from "./project.ts"; -export type { Locale, FieldDefinition, FieldKey, 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 { Locale, FieldDefinition, FieldKey, StructuredDataSource, EntryValueProperty, SiteValueProperty, Section, BodyProfile, ActorKind, BodyBlocks, SurfaceComponentId, Path, Section1, TimeZone, Site, Domain, Localization, CollectionSettings, CollectionLabel, TextField, FieldLabels, FieldLabel, FieldMigration, RichTextField, NumberField, BooleanField, DateField, DateTimeField, UrlField, ImageField, SelectField, FieldOption, CollectionExpiry, StructuredDataPreset, FieldValueSource, EntryValueSource, SiteValueSource, TextValueSource, ThemeRef, Design, Brand, ImageRef, SocialLink, Chrome, ChromeHeader, HeaderAction, ChromeFooter, FooterColumn, HeroSection, Link, RichTextSection, BodyBlock, Actor, ImageSection, ContentListSection, FieldCondition, FieldOrder, ComponentSection, SurfaceSection, SurfaceComponent, Announcement, Consent } from "./site.ts"; export type { 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/page.ts b/packages/core-contracts/src/generated/page.ts index 928779dc..6934d43e 100644 --- a/packages/core-contracts/src/generated/page.ts +++ b/packages/core-contracts/src/generated/page.ts @@ -237,6 +237,223 @@ export interface ContentListSection { * Filter expression over term assignments (RFC-0002), e.g. 'category:news+tag:ai' or 'category:news>*'. */ filter?: string; + /** + * RFC-0042: conditions on the collection's list fields, ANDed. + * + * @minItems 1 + * @maxItems 4 + */ + where?: + | [FieldCondition] + | [FieldCondition, FieldCondition] + | [FieldCondition, FieldCondition, FieldCondition] + | [FieldCondition, FieldCondition, FieldCondition, FieldCondition]; + orderBy?: FieldOrder; +} +/** + * RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`. + * + * This interface was referenced by `Page`'s JSON-Schema + * via the `definition` "FieldCondition". + */ +export interface FieldCondition { + /** + * A field key (site.schema.json FieldKey's pattern, repeated). + */ + field: string; + op: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "set" | "unset"; + value?: string; + /** + * @minItems 1 + * @maxItems 20 + */ + values?: + | [string] + | [string, string] + | [string, string, string] + | [string, string, string, string] + | [string, string, string, string, string] + | [string, string, string, string, string, string] + | [string, string, string, string, string, string, string] + | [string, string, string, string, string, string, string, string] + | [string, string, string, string, string, string, string, string, string] + | [string, string, string, string, string, string, string, string, string, string] + | [string, string, string, string, string, string, string, string, string, string, string] + | [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ] + | [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ] + | [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ] + | [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ] + | [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ] + | [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ] + | [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ] + | [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ] + | [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ]; +} +/** + * RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last. + * + * This interface was referenced by `Page`'s JSON-Schema + * via the `definition` "FieldOrder". + */ +export interface FieldOrder { + /** + * A field key (site.schema.json FieldKey's pattern, repeated). + */ + field: string; + direction: "asc" | "desc"; } /** * 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. diff --git a/packages/core-contracts/src/generated/schemas.ts b/packages/core-contracts/src/generated/schemas.ts index 51cd9585..7f089a10 100644 --- a/packages/core-contracts/src/generated/schemas.ts +++ b/packages/core-contracts/src/generated/schemas.ts @@ -559,6 +559,34 @@ export const SiteSchema = { } } }, + "CollectionExpiry": { + "type": "object", + "description": "RFC-0042: a time gate every entry derives from one of its date or datetime fields, checked at render time like Availability.until. A date ends at the start of the next day in the site's time zone.", + "additionalProperties": false, + "required": [ + "field", + "action" + ], + "properties": { + "field": { + "$ref": "#/$defs/FieldKey" + }, + "action": { + "type": "string", + "description": "unlist: the page stays, the entry leaves listings, feeds, the sitemap and search. hide: every public route answers 404.", + "enum": [ + "unlist", + "hide" + ] + }, + "afterMinutes": { + "type": "integer", + "description": "Grace after the field's instant. Absent: 0.", + "minimum": 0, + "maximum": 10080 + } + } + }, "CollectionLabel": { "type": "object", "additionalProperties": false, @@ -634,6 +662,22 @@ export const SiteSchema = { "items": { "$ref": "#/$defs/FieldDefinition" } + }, + "listFields": { + "type": "array", + "description": "RFC-0042: fields that listings, queries, Studio columns and MCP may filter and sort by, in column order. Keys of this collection's fields (checked in code).", + "minItems": 1, + "maxItems": 4, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/FieldKey" + } + }, + "expiry": { + "$ref": "#/$defs/CollectionExpiry" + }, + "structuredData": { + "$ref": "#/$defs/StructuredDataPreset" } } }, @@ -690,6 +734,65 @@ export const SiteSchema = { } } }, + "EntryValueProperty": { + "type": "string", + "description": "RFC-0042: entry properties a structured-data property may read.", + "enum": [ + "title", + "summary", + "body", + "coverImage", + "url", + "publishedAt", + "updatedAt" + ] + }, + "FieldCondition": { + "type": "object", + "description": "RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`.", + "additionalProperties": false, + "required": [ + "field", + "op" + ], + "properties": { + "field": { + "type": "string", + "description": "A field key (site.schema.json FieldKey's pattern, repeated).", + "pattern": "^[a-z][A-Za-z0-9]{0,39}$" + }, + "op": { + "type": "string", + "enum": [ + "eq", + "ne", + "lt", + "lte", + "gt", + "gte", + "in", + "set", + "unset" + ] + }, + "value": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "values": { + "type": "array", + "minItems": 1, + "maxItems": 20, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 100 + } + } + } + }, "FieldDefinition": { "description": "RFC-0041: one typed custom field of a collection, discriminated by `type`.", "oneOf": [ @@ -1160,6 +1263,29 @@ export const SiteSchema = { } } }, + "FieldOrder": { + "type": "object", + "description": "RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last.", + "additionalProperties": false, + "required": [ + "field", + "direction" + ], + "properties": { + "field": { + "type": "string", + "description": "A field key (site.schema.json FieldKey's pattern, repeated).", + "pattern": "^[a-z][A-Za-z0-9]{0,39}$" + }, + "direction": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + } + }, "FooterColumn": { "type": "object", "additionalProperties": false, @@ -1516,6 +1642,18 @@ export const SiteSchema = { "description": "Filter expression over term assignments (RFC-0002), e.g. 'category:news+tag:ai' or 'category:news>*'.", "minLength": 1, "maxLength": 512 + }, + "where": { + "type": "array", + "description": "RFC-0042: conditions on the collection's list fields, ANDed.", + "minItems": 1, + "maxItems": 4, + "items": { + "$ref": "#/$defs/FieldCondition" + } + }, + "orderBy": { + "$ref": "#/$defs/FieldOrder" } } }, @@ -1594,6 +1732,15 @@ export const SiteSchema = { } ] }, + "SiteValueProperty": { + "type": "string", + "description": "RFC-0042: site properties a structured-data property may read.", + "enum": [ + "name", + "url", + "logo" + ] + }, "Slug": { "type": "string", "description": "Lower-case URL segment.", @@ -1633,6 +1780,131 @@ export const SiteSchema = { } } }, + "StructuredDataPreset": { + "type": "object", + "description": "RFC-0042: the JSON-LD type an entry page emits in place of BlogPosting, with its properties mapped to value sources. Paths, fitting field types and required properties are a closed table in code (@cloudhal/core-ts/structured-data).", + "additionalProperties": false, + "required": [ + "type", + "properties" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "Event", + "JobPosting", + "Product", + "Recipe" + ] + }, + "properties": { + "type": "object", + "description": "Keyed by property path, e.g. startDate or location.address.addressLocality.", + "propertyNames": { + "type": "string", + "pattern": "^[a-z][A-Za-z]{0,39}(\\.[a-z][A-Za-z]{0,39}){0,2}$" + }, + "minProperties": 1, + "maxProperties": 40, + "additionalProperties": { + "$ref": "#/$defs/StructuredDataSource" + } + } + } + }, + "StructuredDataSource": { + "description": "RFC-0042: where one JSON-LD property's value comes from, discriminated by `kind`.", + "oneOf": [ + { + "title": "FieldValueSource", + "type": "object", + "description": "A typed field of this collection, converted by its type.", + "additionalProperties": false, + "required": [ + "kind", + "field" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "field" + ] + }, + "field": { + "$ref": "#/$defs/FieldKey" + } + } + }, + { + "title": "EntryValueSource", + "type": "object", + "description": "A built-in property of the entry.", + "additionalProperties": false, + "required": [ + "kind", + "property" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "entry" + ] + }, + "property": { + "$ref": "#/$defs/EntryValueProperty" + } + } + }, + { + "title": "SiteValueSource", + "type": "object", + "description": "A property of the site.", + "additionalProperties": false, + "required": [ + "kind", + "property" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "site" + ] + }, + "property": { + "$ref": "#/$defs/SiteValueProperty" + } + } + }, + { + "title": "TextValueSource", + "type": "object", + "description": "A plain-text constant: no control characters, no markup meaning; vocabulary-checked on enum paths (in code).", + "additionalProperties": false, + "required": [ + "kind", + "value" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "text" + ] + }, + "value": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "pattern": "^[^\\x00-\\x1F\\x7F]+$" + } + } + } + ] + }, "SurfaceComponent": { "type": "object", "description": "One element of a generative UI surface (RFC-0020 §1), in a flat list: `component` names a catalog entry (a PascalCase built-in primitive such as `Stack`, or a registry block type such as `callout`), `children` refer to other ids of the same surface, and `props` follow the entry's schema, which the store checks. Field names follow A2UI v0.9.", @@ -1913,6 +2185,75 @@ export const PageSchema = { ], "description": "The markdown dialect a body follows (RFC-0014 §1): 1 = Cloudhal Markdown v1 (CommonMark + GFM + `asset:` + directives; raw HTML escaped). Set by the store on markdown bodies." }, + "FieldCondition": { + "type": "object", + "description": "RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`.", + "additionalProperties": false, + "required": [ + "field", + "op" + ], + "properties": { + "field": { + "type": "string", + "description": "A field key (site.schema.json FieldKey's pattern, repeated).", + "pattern": "^[a-z][A-Za-z0-9]{0,39}$" + }, + "op": { + "type": "string", + "enum": [ + "eq", + "ne", + "lt", + "lte", + "gt", + "gte", + "in", + "set", + "unset" + ] + }, + "value": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "values": { + "type": "array", + "minItems": 1, + "maxItems": 20, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 100 + } + } + } + }, + "FieldOrder": { + "type": "object", + "description": "RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last.", + "additionalProperties": false, + "required": [ + "field", + "direction" + ], + "properties": { + "field": { + "type": "string", + "description": "A field key (site.schema.json FieldKey's pattern, repeated).", + "pattern": "^[a-z][A-Za-z0-9]{0,39}$" + }, + "direction": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + } + }, "Id": { "type": "string", "description": "Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.", @@ -2169,6 +2510,18 @@ export const PageSchema = { "description": "Filter expression over term assignments (RFC-0002), e.g. 'category:news+tag:ai' or 'category:news>*'.", "minLength": 1, "maxLength": 512 + }, + "where": { + "type": "array", + "description": "RFC-0042: conditions on the collection's list fields, ANDed.", + "minItems": 1, + "maxItems": 4, + "items": { + "$ref": "#/$defs/FieldCondition" + } + }, + "orderBy": { + "$ref": "#/$defs/FieldOrder" } } }, @@ -2604,6 +2957,18 @@ export const SectionSchema = { "description": "Filter expression over term assignments (RFC-0002), e.g. 'category:news+tag:ai' or 'category:news>*'.", "minLength": 1, "maxLength": 512 + }, + "where": { + "type": "array", + "description": "RFC-0042: conditions on the collection's list fields, ANDed.", + "minItems": 1, + "maxItems": 4, + "items": { + "$ref": "#/$defs/FieldCondition" + } + }, + "orderBy": { + "$ref": "#/$defs/FieldOrder" } } }, @@ -2765,6 +3130,75 @@ export const SectionSchema = { ], "description": "The markdown dialect a body follows (RFC-0014 §1): 1 = Cloudhal Markdown v1 (CommonMark + GFM + `asset:` + directives; raw HTML escaped). Set by the store on markdown bodies." }, + "FieldCondition": { + "type": "object", + "description": "RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`.", + "additionalProperties": false, + "required": [ + "field", + "op" + ], + "properties": { + "field": { + "type": "string", + "description": "A field key (site.schema.json FieldKey's pattern, repeated).", + "pattern": "^[a-z][A-Za-z0-9]{0,39}$" + }, + "op": { + "type": "string", + "enum": [ + "eq", + "ne", + "lt", + "lte", + "gt", + "gte", + "in", + "set", + "unset" + ] + }, + "value": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "values": { + "type": "array", + "minItems": 1, + "maxItems": 20, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 100 + } + } + } + }, + "FieldOrder": { + "type": "object", + "description": "RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last.", + "additionalProperties": false, + "required": [ + "field", + "direction" + ], + "properties": { + "field": { + "type": "string", + "description": "A field key (site.schema.json FieldKey's pattern, repeated).", + "pattern": "^[a-z][A-Za-z0-9]{0,39}$" + }, + "direction": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + } + }, "Id": { "type": "string", "description": "Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.", diff --git a/packages/core-contracts/src/generated/section.ts b/packages/core-contracts/src/generated/section.ts index 6f5755c3..0b30a9d1 100644 --- a/packages/core-contracts/src/generated/section.ts +++ b/packages/core-contracts/src/generated/section.ts @@ -123,6 +123,217 @@ export interface ContentListSection { * Filter expression over term assignments (RFC-0002), e.g. 'category:news+tag:ai' or 'category:news>*'. */ filter?: string; + /** + * RFC-0042: conditions on the collection's list fields, ANDed. + * + * @minItems 1 + * @maxItems 4 + */ + where?: + | [FieldCondition] + | [FieldCondition, FieldCondition] + | [FieldCondition, FieldCondition, FieldCondition] + | [FieldCondition, FieldCondition, FieldCondition, FieldCondition]; + orderBy?: FieldOrder; +} +/** + * RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`. + */ +export interface FieldCondition { + /** + * A field key (site.schema.json FieldKey's pattern, repeated). + */ + field: string; + op: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "set" | "unset"; + value?: string; + /** + * @minItems 1 + * @maxItems 20 + */ + values?: + | [string] + | [string, string] + | [string, string, string] + | [string, string, string, string] + | [string, string, string, string, string] + | [string, string, string, string, string, string] + | [string, string, string, string, string, string, string] + | [string, string, string, string, string, string, string, string] + | [string, string, string, string, string, string, string, string, string] + | [string, string, string, string, string, string, string, string, string, string] + | [string, string, string, string, string, string, string, string, string, string, string] + | [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ] + | [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ] + | [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ] + | [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ] + | [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ] + | [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ] + | [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ] + | [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ] + | [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ]; +} +/** + * RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last. + */ +export interface FieldOrder { + /** + * A field key (site.schema.json FieldKey's pattern, repeated). + */ + field: string; + direction: "asc" | "desc"; } /** * 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. diff --git a/packages/core-contracts/src/generated/site.ts b/packages/core-contracts/src/generated/site.ts index 91c90970..4aacd250 100644 --- a/packages/core-contracts/src/generated/site.ts +++ b/packages/core-contracts/src/generated/site.ts @@ -56,6 +56,29 @@ export type FieldDefinition = * via the `definition` "FieldKey". */ export type FieldKey = string; +/** + * RFC-0042: where one JSON-LD property's value comes from, discriminated by `kind`. + * + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "StructuredDataSource". + */ +export type StructuredDataSource = + FieldValueSource | EntryValueSource | SiteValueSource | TextValueSource; +/** + * RFC-0042: entry properties a structured-data property may read. + * + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "EntryValueProperty". + */ +export type EntryValueProperty = + "title" | "summary" | "body" | "coverImage" | "url" | "publishedAt" | "updatedAt"; +/** + * RFC-0042: site properties a structured-data property may read. + * + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "SiteValueProperty". + */ +export type SiteValueProperty = "name" | "url" | "logo"; /** * 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. */ @@ -260,6 +283,19 @@ export interface CollectionSettings { * @maxItems 30 */ fields?: [FieldDefinition, ...FieldDefinition[]]; + /** + * RFC-0042: fields that listings, queries, Studio columns and MCP may filter and sort by, in column order. Keys of this collection's fields (checked in code). + * + * @minItems 1 + * @maxItems 4 + */ + listFields?: + | [FieldKey] + | [FieldKey, FieldKey] + | [FieldKey, FieldKey, FieldKey] + | [FieldKey, FieldKey, FieldKey, FieldKey]; + expiry?: CollectionExpiry; + structuredData?: StructuredDataPreset; } /** * This interface was referenced by `Site`'s JSON-Schema @@ -498,6 +534,66 @@ export interface FieldOption { [k: string]: string | undefined; }; } +/** + * RFC-0042: a time gate every entry derives from one of its date or datetime fields, checked at render time like Availability.until. A date ends at the start of the next day in the site's time zone. + * + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "CollectionExpiry". + */ +export interface CollectionExpiry { + field: FieldKey; + /** + * unlist: the page stays, the entry leaves listings, feeds, the sitemap and search. hide: every public route answers 404. + */ + action: "unlist" | "hide"; + /** + * Grace after the field's instant. Absent: 0. + */ + afterMinutes?: number; +} +/** + * RFC-0042: the JSON-LD type an entry page emits in place of BlogPosting, with its properties mapped to value sources. Paths, fitting field types and required properties are a closed table in code (@cloudhal/core-ts/structured-data). + * + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "StructuredDataPreset". + */ +export interface StructuredDataPreset { + type: "Event" | "JobPosting" | "Product" | "Recipe"; + /** + * Keyed by property path, e.g. startDate or location.address.addressLocality. + */ + properties: { + [k: string]: StructuredDataSource | undefined; + }; +} +/** + * A typed field of this collection, converted by its type. + */ +export interface FieldValueSource { + kind: "field"; + field: FieldKey; +} +/** + * A built-in property of the entry. + */ +export interface EntryValueSource { + kind: "entry"; + property: EntryValueProperty; +} +/** + * A property of the site. + */ +export interface SiteValueSource { + kind: "site"; + property: SiteValueProperty; +} +/** + * A plain-text constant: no control characters, no markup meaning; vocabulary-checked on enum paths (in code). + */ +export interface TextValueSource { + kind: "text"; + value: string; +} /** * Theme (a.k.a. Experience) applied by the renderer. Config is theme-defined and validated by the theme's own schema, not here. * @@ -769,6 +865,223 @@ export interface ContentListSection { * Filter expression over term assignments (RFC-0002), e.g. 'category:news+tag:ai' or 'category:news>*'. */ filter?: string; + /** + * RFC-0042: conditions on the collection's list fields, ANDed. + * + * @minItems 1 + * @maxItems 4 + */ + where?: + | [FieldCondition] + | [FieldCondition, FieldCondition] + | [FieldCondition, FieldCondition, FieldCondition] + | [FieldCondition, FieldCondition, FieldCondition, FieldCondition]; + orderBy?: FieldOrder; +} +/** + * RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`. + * + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "FieldCondition". + */ +export interface FieldCondition { + /** + * A field key (site.schema.json FieldKey's pattern, repeated). + */ + field: string; + op: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "set" | "unset"; + value?: string; + /** + * @minItems 1 + * @maxItems 20 + */ + values?: + | [string] + | [string, string] + | [string, string, string] + | [string, string, string, string] + | [string, string, string, string, string] + | [string, string, string, string, string, string] + | [string, string, string, string, string, string, string] + | [string, string, string, string, string, string, string, string] + | [string, string, string, string, string, string, string, string, string] + | [string, string, string, string, string, string, string, string, string, string] + | [string, string, string, string, string, string, string, string, string, string, string] + | [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ] + | [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ] + | [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ] + | [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ] + | [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ] + | [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ] + | [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ] + | [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ] + | [ + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + ]; +} +/** + * RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last. + * + * This interface was referenced by `Site`'s JSON-Schema + * via the `definition` "FieldOrder". + */ +export interface FieldOrder { + /** + * A field key (site.schema.json FieldKey's pattern, repeated). + */ + field: string; + direction: "asc" | "desc"; } /** * 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. diff --git a/packages/core-contracts/src/generated/validators.js b/packages/core-contracts/src/generated/validators.js index 3474c469..fba42f20 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"]},"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 schema41 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://cloudhal.dev/schemas/v1/site.schema.json","title":"Site","description":"Root node of a Site Graph: one deployable website with its locales, domains, theme and page index. Pages and content entries are separate nodes referenced by id so they can be versioned and locked independently.","type":"object","additionalProperties":false,"required":["schemaVersion","id","projectId","name","slug","defaultLocale","locales","domains","pageIds","theme","rev","createdAt","updatedAt"],"properties":{"schemaVersion":{"$ref":"#/$defs/SchemaVersion"},"id":{"$ref":"#/$defs/Id"},"projectId":{"$ref":"#/$defs/Id"},"name":{"type":"string","minLength":1,"maxLength":200},"slug":{"$ref":"#/$defs/Slug"},"defaultLocale":{"$ref":"#/$defs/Locale"},"locales":{"type":"array","items":{"$ref":"#/$defs/Locale"},"minItems":1,"uniqueItems":true},"domains":{"type":"array","description":"Hostnames serving this site. Exactly one should be primary: the address canonical links, feeds and previews use. Sites run on their own domains or their Cloudflare account's workers.dev address, never on a Cloudhal domain (ADR-0037).","items":{"$ref":"#/$defs/Domain"}},"pageIds":{"type":"array","items":{"$ref":"#/$defs/Id"},"uniqueItems":true},"homePageId":{"$ref":"#/$defs/Id"},"localization":{"$ref":"#/$defs/Localization"},"collections":{"type":"object","description":"RFC-0038, RFC-0040: the site's declared collections and their settings, keyed by collection (ContentEntry.collection). A key here declares the collection, even with no settings ({}). A collection with entries but no key is inferred and uses the defaults.","propertyNames":{"$ref":"#/$defs/Slug"},"maxProperties":50,"additionalProperties":{"$ref":"#/$defs/CollectionSettings"}},"theme":{"$ref":"#/$defs/ThemeRef"},"design":{"$ref":"#/$defs/Design"},"brand":{"$ref":"#/$defs/Brand"},"chrome":{"$ref":"#/$defs/Chrome"},"timeZone":{"$ref":"#/$defs/TimeZone","description":"The site's time zone (RFC-0008): Studio's calendar, schedules entered as wall-clock times and recurring automations use it. Absent means UTC."},"publishedSnapshotId":{"$ref":"#/$defs/Id","description":"Snapshot currently serving production. Rollback = pointing this at an older snapshot."},"rev":{"$ref":"#/$defs/Revision"},"createdAt":{"$ref":"#/$defs/Timestamp"},"updatedAt":{"$ref":"#/$defs/Timestamp"},"metadata":{"$ref":"#/$defs/Metadata"}},"$defs":{"Actor":{"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}},"ActorKind":{"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]},"Announcement":{"type":"object","additionalProperties":false,"required":["id","text"],"properties":{"id":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,40}$","description":"Change it when the text changes, so readers who dismissed the old one see the new one."},"text":{"type":"string","minLength":1,"maxLength":200},"label":{"type":"string","minLength":1,"maxLength":40},"href":{"type":"string","minLength":1,"maxLength":2048},"tone":{"type":"string","enum":["info","brand","warning"]},"dismissible":{"type":"boolean"}}},"BodyBlock":{"type":"object","description":"One top-level block of a markdown body in the store's index (RFC-0014 §3). Written by the store only: a client's values are replaced on every write. `by`, `via` and `at` say who last changed the block's content, through which channel and when; they are absent for content written before the index existed.","additionalProperties":false,"required":["id","hash"],"properties":{"id":{"type":"string","pattern":"^blk_[0-9A-Za-z]{8,24}$","description":"Stable across writes that keep the block's content or its place among unchanged blocks."},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"SHA-256 (hex) of the block's markdown without trailing whitespace."},"by":{"$ref":"#/$defs/Actor"},"via":{"type":"string","enum":["studio","mcp","api","import","ai-command","proposal"],"description":"The channel of that change."},"at":{"$ref":"#/$defs/Timestamp"}}},"BodyBlocks":{"type":"array","description":"A markdown body's block index, in body order (RFC-0014 §3). Absent means not indexed (yet, or an HTML body), never empty.","maxItems":5000,"items":{"$ref":"#/$defs/BodyBlock"}},"BodyProfile":{"type":"integer","enum":[1],"description":"The markdown dialect a body follows (RFC-0014 §1): 1 = Cloudhal Markdown v1 (CommonMark + GFM + `asset:` + directives; raw HTML escaped). Set by the store on markdown bodies."},"Brand":{"type":"object","description":"The site's logo and social links (RFC-0025 §1), shared by the header, the footer and SEO.","additionalProperties":false,"properties":{"logo":{"$ref":"#/$defs/ImageRef"},"logoDark":{"$ref":"#/$defs/ImageRef"},"logoHeight":{"type":"string","enum":["s","m","l"]},"social":{"type":"array","maxItems":8,"items":{"$ref":"#/$defs/SocialLink"}}}},"Chrome":{"type":"object","description":"The site's header, footer and bars as data (RFC-0025): enums, menu keys and short texts; the theme renders them with landmarks, focus and contrast guaranteed.","additionalProperties":false,"properties":{"header":{"$ref":"#/$defs/ChromeHeader"},"footer":{"$ref":"#/$defs/ChromeFooter"},"announcement":{"$ref":"#/$defs/Announcement"},"consent":{"$ref":"#/$defs/Consent"}}},"ChromeFooter":{"type":"object","additionalProperties":false,"properties":{"layout":{"type":"string","enum":["simple","columns","cta-band","minimal"]},"surface":{"type":"string","enum":["plain","muted","inverse"]},"tagline":{"type":"string","minLength":1,"maxLength":200},"columns":{"type":"array","maxItems":5,"items":{"$ref":"#/$defs/FooterColumn"}},"ctaHeading":{"type":"string","minLength":1,"maxLength":120},"ctaLabel":{"type":"string","minLength":1,"maxLength":40},"ctaHref":{"type":"string","minLength":1,"maxLength":2048},"social":{"type":"boolean"},"copyright":{"type":"string","minLength":1,"maxLength":200,"description":"The legal line; {year} and {site} are the only placeholders."},"legal":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."},"extras":{"type":"array","maxItems":3,"uniqueItems":true,"items":{"type":"string","enum":["locale","theme-toggle","motion-toggle"]}},"credit":{"type":"boolean"},"region":{"$ref":"#/$defs/Section","description":"RFC-0025 §2: one optional surface (kind `surface`) above the footer's columns, built and checked like any surface; the store refuses other section kinds."}}},"ChromeHeader":{"type":"object","additionalProperties":false,"properties":{"layout":{"type":"string","enum":["logo-left","logo-left-nav-center","centered","split","minimal"]},"behavior":{"type":"string","enum":["static","sticky","hide-on-scroll","fixed"]},"shrink":{"type":"boolean"},"overHero":{"type":"string","enum":["solid","transparent"]},"width":{"type":"string","enum":["content","wide","full"]},"surface":{"type":"string","enum":["plain","muted","raised","inverse","brand"]},"mobileStyle":{"type":"string","enum":["dropdown","drawer-start","drawer-end","fullscreen"]},"collapseAt":{"type":"string","enum":["sm","md","lg","always"]},"nav":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."},"utility":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."},"actions":{"type":"array","maxItems":3,"items":{"$ref":"#/$defs/HeaderAction"}}}},"CollectionExpiry":{"type":"object","description":"RFC-0042: a time gate every entry derives from one of its date or datetime fields, checked at render time like Availability.until. A date ends at the start of the next day in the site's time zone.","additionalProperties":false,"required":["field","action"],"properties":{"field":{"$ref":"#/$defs/FieldKey"},"action":{"type":"string","description":"unlist: the page stays, the entry leaves listings, feeds, the sitemap and search. hide: every public route answers 404.","enum":["unlist","hide"]},"afterMinutes":{"type":"integer","description":"Grace after the field's instant. Absent: 0.","minimum":0,"maximum":10080}}},"CollectionLabel":{"type":"object","additionalProperties":false,"required":["singular","plural"],"properties":{"singular":{"type":"string","minLength":1,"maxLength":100},"plural":{"type":"string","minLength":1,"maxLength":100},"description":{"type":"string","maxLength":1000}}},"CollectionSettings":{"type":"object","additionalProperties":false,"properties":{"permalink":{"type":"string","description":"RFC-0038: where the collection's entries live, as a template with a closed token set. Version 1: one or more fixed lower-case path segments followed by /{slug}, e.g. /blog/{slug}. Absent means //{slug}.","pattern":"^(/[a-z0-9]+(-[a-z0-9]+)*)+/\\{slug\\}$"},"labels":{"type":"object","description":"RFC-0040: display names per locale (Studio, agents, themes). Absent or missing a locale: the default locale's, then the key.","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"$ref":"#/$defs/CollectionLabel"}},"icon":{"type":"string","description":"RFC-0040: a name from Studio's closed icon set (e.g. calendar, briefcase, users). Unknown names show the default icon. Never a URL or markup.","pattern":"^[a-z0-9]+(-[a-z0-9]+)*$","maxLength":40},"feed":{"type":"boolean","description":"RFC-0040: entries appear in the site's RSS feeds. Absent: true."},"sitemap":{"type":"boolean","description":"RFC-0040: published entries appear in sitemap.xml. Absent: true."},"sort":{"type":"string","description":"RFC-0040: default order where none is given (Studio's list, entry queries without sort, MCP list_entries). Absent: newest.","enum":["newest","oldest","title"]},"fields":{"type":"array","description":"RFC-0041: the collection's typed custom fields, in form order. Values live in ContentEntry.fields under each key. At most 200 per site and unique keys (checked in code).","minItems":1,"maxItems":30,"items":{"$ref":"#/$defs/FieldDefinition"}},"listFields":{"type":"array","description":"RFC-0042: fields that listings, queries, Studio columns and MCP may filter and sort by, in column order. Keys of this collection's fields (checked in code).","minItems":1,"maxItems":4,"uniqueItems":true,"items":{"$ref":"#/$defs/FieldKey"}},"expiry":{"$ref":"#/$defs/CollectionExpiry"},"structuredData":{"$ref":"#/$defs/StructuredDataPreset"}}},"Consent":{"type":"object","additionalProperties":false,"required":["mode"],"properties":{"mode":{"type":"string","enum":["none","notice","choices"]},"policy":{"$ref":"#/$defs/Path"}}},"Design":{"type":"object","description":"The site's design rules for people and agents (RFC-0021): Markdown that agents read with the theme's DESIGN.md before they build. Guidance only: it changes neither rendering nor permissions.","additionalProperties":false,"required":["markdown"],"properties":{"markdown":{"type":"string","minLength":1,"maxLength":24000}}},"Domain":{"type":"object","additionalProperties":false,"required":["hostname","primary"],"properties":{"hostname":{"type":"string","pattern":"^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,63}$","maxLength":253},"primary":{"type":"boolean"}}},"EntryValueProperty":{"type":"string","description":"RFC-0042: entry properties a structured-data property may read.","enum":["title","summary","body","coverImage","url","publishedAt","updatedAt"]},"FieldCondition":{"type":"object","description":"RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`.","additionalProperties":false,"required":["field","op"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","in","set","unset"]},"value":{"type":"string","minLength":1,"maxLength":100},"values":{"type":"array","minItems":1,"maxItems":20,"uniqueItems":true,"items":{"type":"string","minLength":1,"maxLength":100}}}},"FieldDefinition":{"description":"RFC-0041: one typed custom field of a collection, discriminated by `type`.","oneOf":[{"title":"TextField","type":"object","description":"Plain text: a string of at most maxLength characters.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["text"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"maxLength":{"type":"integer","description":"Absent: 5000.","minimum":1,"maximum":5000},"multiline":{"type":"boolean","description":"Line breaks allowed. Absent: false."},"searchable":{"type":"boolean","description":"Its words go into site search. Absent: false."}}},{"title":"RichTextField","type":"object","description":"Cloudhal Markdown v1 (ADR-0028), at most 50 000 characters.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["richText"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"searchable":{"type":"boolean","description":"Its words go into site search. Absent: false."}}},{"title":"NumberField","type":"object","description":"A finite number.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["number"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"integer":{"type":"boolean","description":"Whole numbers only. Absent: false."},"minimum":{"type":"number"},"maximum":{"type":"number"}}},{"title":"BooleanField","type":"object","description":"true or false; absent reads as false, so it cannot be required to publish.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["boolean"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"DateField","type":"object","description":"A calendar day without a zone (YYYY-MM-DD).","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["date"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"DateTimeField","type":"object","description":"An instant with the zone it was entered in: {at, timeZone} (RFC-0008 §9.1).","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["datetime"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"UrlField","type":"object","description":"A link: https, http, mailto, tel, a site-relative path or a #fragment; at most 2048 characters.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["url"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"ImageField","type":"object","description":"An image Asset of the same site, by id (asset_…).","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["image"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"SelectField","type":"object","description":"One of a closed list of options, or several when multiple.","additionalProperties":false,"required":["key","type","labels","localized","options"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["select"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"options":{"type":"array","minItems":1,"maxItems":50,"items":{"$ref":"#/$defs/FieldOption"}},"multiple":{"type":"boolean","description":"The value is a list of option values. Absent: false."}}}]},"FieldKey":{"type":"string","description":"The field's key in ContentEntry.fields: lower camelCase, a JavaScript identifier. Unique in its collection; built-in entry property names are reserved (checked in code).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"FieldLabel":{"type":"object","additionalProperties":false,"required":["label"],"properties":{"label":{"type":"string","minLength":1,"maxLength":100},"help":{"type":"string","maxLength":300}}},"FieldLabels":{"type":"object","description":"The field's label and help per locale. Missing a locale: the default locale's, then the key.","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"$ref":"#/$defs/FieldLabel"}},"FieldMigration":{"type":"object","description":"RFC-0041 §6: present only while a field operation rewrites entries.","additionalProperties":false,"required":["id"],"properties":{"id":{"$ref":"#/$defs/Id","description":"The operation's \"before\" snapshot."},"fromKey":{"$ref":"#/$defs/FieldKey"},"fromType":{"type":"string","enum":["text","richText","number","boolean","date","datetime","url","image","select"]},"fromLocalized":{"type":"boolean"}}},"FieldOption":{"type":"object","description":"One option of a select field. Values are unique in the field (checked in code).","additionalProperties":false,"required":["value","labels"],"properties":{"value":{"$ref":"#/$defs/Slug","description":"Stored in entries, never shown."},"labels":{"type":"object","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"type":"string","minLength":1,"maxLength":100}}}},"FieldOrder":{"type":"object","description":"RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last.","additionalProperties":false,"required":["field","direction"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"direction":{"type":"string","enum":["asc","desc"]}}},"FooterColumn":{"type":"object","additionalProperties":false,"required":["heading","menu"],"properties":{"heading":{"type":"string","minLength":1,"maxLength":60},"menu":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."}}},"HeaderAction":{"type":"object","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["button","search","locale","theme-toggle","motion-toggle"]},"label":{"type":"string","minLength":1,"maxLength":40},"href":{"type":"string","minLength":1,"maxLength":2048},"variant":{"type":"string","enum":["primary","secondary"]}}},"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"ImageRef":{"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}},"Link":{"type":"object","additionalProperties":false,"required":["label","href"],"properties":{"label":{"type":"string","minLength":1,"maxLength":200},"href":{"type":"string","minLength":1,"maxLength":2048},"external":{"type":"boolean"}}},"Locale":{"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"},"Localization":{"type":"object","description":"How the site serves its locales (RFC-0033 §2). Absent fields mean: prefix-except-default routing, no fallback, no required review, the machine-translation notice until reviewed.","additionalProperties":false,"properties":{"routing":{"type":"string","description":"prefix-except-default: the default locale at the root, the others under //. prefix-all: every locale prefixed, / is a language chooser (x-default).","enum":["prefix-except-default","prefix-all"]},"domains":{"type":"object","description":"Optional hostname per locale; that locale is then served at the host's root.","propertyNames":{"$ref":"#/$defs/Locale"},"additionalProperties":{"type":"string","pattern":"^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,63}$","maxLength":253}},"fallback":{"type":"string","description":"none: a missing translation is not found. default-with-notice: the default locale's page is served with a notice and noindex.","enum":["none","default-with-notice"]},"review":{"type":"object","description":"Collections whose machine translations need a person's review before they publish ('pages' stands for pages).","additionalProperties":false,"properties":{"required":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"uniqueItems":true}}},"label":{"type":"string","description":"When readers see the machine-translation notice: unreviewed (until a person reviews) or always.","enum":["unreviewed","always"]}}},"Metadata":{"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}},"Path":{"type":"string","description":"Absolute URL path of a route the site owns: starts with '/', lower-case slug segments, no trailing slash except root.","pattern":"^/(?:[a-z0-9]+(?:-[a-z0-9]+)*(?:/[a-z0-9]+(?:-[a-z0-9]+)*)*)?$","maxLength":1024},"Revision":{"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0},"SchemaVersion":{"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]},"Section":{"title":"Section","description":"A block inside a page, discriminated by `kind`. Variants are written inline (not via $ref) and `kind` is a single-value string enum so that TypeScript gets a literal union and Rust gets a serde internally-tagged enum.","oneOf":[{"type":"object","title":"HeroSection","additionalProperties":false,"required":["id","kind","heading"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["hero"]},"heading":{"type":"string","minLength":1,"maxLength":300},"subheading":{"type":"string","maxLength":1000},"image":{"$ref":"#/$defs/ImageRef"},"cta":{"$ref":"#/$defs/Link"}}},{"type":"object","title":"RichTextSection","additionalProperties":false,"required":["id","kind","format","body"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["richText"]},"format":{"type":"string","enum":["markdown","html"]},"body":{"type":"string","maxLength":200000},"profile":{"$ref":"#/$defs/BodyProfile"},"blocks":{"$ref":"#/$defs/BodyBlocks"}}},{"type":"object","title":"ImageSection","additionalProperties":false,"required":["id","kind","image"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["image"]},"image":{"$ref":"#/$defs/ImageRef"},"caption":{"type":"string","maxLength":1000}}},{"type":"object","title":"ContentListSection","description":"Renders the newest entries of a content collection (e.g. blog posts).","additionalProperties":false,"required":["id","kind","collection","limit","sort"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["contentList"]},"collection":{"$ref":"#/$defs/Slug"},"limit":{"type":"integer","minimum":1,"maximum":100},"sort":{"type":"string","enum":["newest","oldest","title"]},"filter":{"type":"string","description":"Filter expression over term assignments (RFC-0002), e.g. 'category:news+tag:ai' or 'category:news>*'.","minLength":1,"maxLength":512},"where":{"type":"array","description":"RFC-0042: conditions on the collection's list fields, ANDed.","minItems":1,"maxItems":4,"items":{"$ref":"#/$defs/FieldCondition"}},"orderBy":{"$ref":"#/$defs/FieldOrder"}}},{"type":"object","title":"ComponentSection","description":"A typed block from the block registry (RFC-0015): `type` names a registered block type (one global namespace of English names), `props` follow that type's props schema, which the store validates.","additionalProperties":false,"required":["id","kind","type","props"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["component"]},"type":{"type":"string","pattern":"^[a-z][a-z0-9-]{1,40}$"},"props":{"type":"object","description":"Validated against the type's props schema by the store, not by this contract.","additionalProperties":true}}},{"type":"object","title":"SurfaceSection","description":"A generative UI surface (RFC-0020): a flat list of catalog components with a `root`, plus a data model their props may bind to. Written and streamed by agents as data, never code; the store checks the tree, the catalog and the limits.","additionalProperties":false,"required":["id","kind","catalog","components"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["surface"]},"catalog":{"type":"string","description":"The catalog the surface was written for: `@` (RFC-0018 manifest).","pattern":"^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"},"components":{"type":"array","minItems":1,"maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponent"}},"data":{"type":"object","description":"The surface's data model: JSON its components bind to with `{path}` (at most 64 KB serialized; the store enforces the size).","additionalProperties":true}}}]},"SiteValueProperty":{"type":"string","description":"RFC-0042: site properties a structured-data property may read.","enum":["name","url","logo"]},"Slug":{"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128},"SocialLink":{"type":"object","additionalProperties":false,"required":["network","href"],"properties":{"network":{"type":"string","enum":["x","bluesky","mastodon","github","linkedin","instagram","facebook","youtube","tiktok","threads","rss","email"]},"href":{"type":"string","minLength":1,"maxLength":2048}}},"StructuredDataPreset":{"type":"object","description":"RFC-0042: the JSON-LD type an entry page emits in place of BlogPosting, with its properties mapped to value sources. Paths, fitting field types and required properties are a closed table in code (@cloudhal/core-ts/structured-data).","additionalProperties":false,"required":["type","properties"],"properties":{"type":{"type":"string","enum":["Event","JobPosting","Product","Recipe"]},"properties":{"type":"object","description":"Keyed by property path, e.g. startDate or location.address.addressLocality.","propertyNames":{"type":"string","pattern":"^[a-z][A-Za-z]{0,39}(\\.[a-z][A-Za-z]{0,39}){0,2}$"},"minProperties":1,"maxProperties":40,"additionalProperties":{"$ref":"#/$defs/StructuredDataSource"}}}},"StructuredDataSource":{"description":"RFC-0042: where one JSON-LD property's value comes from, discriminated by `kind`.","oneOf":[{"title":"FieldValueSource","type":"object","description":"A typed field of this collection, converted by its type.","additionalProperties":false,"required":["kind","field"],"properties":{"kind":{"type":"string","enum":["field"]},"field":{"$ref":"#/$defs/FieldKey"}}},{"title":"EntryValueSource","type":"object","description":"A built-in property of the entry.","additionalProperties":false,"required":["kind","property"],"properties":{"kind":{"type":"string","enum":["entry"]},"property":{"$ref":"#/$defs/EntryValueProperty"}}},{"title":"SiteValueSource","type":"object","description":"A property of the site.","additionalProperties":false,"required":["kind","property"],"properties":{"kind":{"type":"string","enum":["site"]},"property":{"$ref":"#/$defs/SiteValueProperty"}}},{"title":"TextValueSource","type":"object","description":"A plain-text constant: no control characters, no markup meaning; vocabulary-checked on enum paths (in code).","additionalProperties":false,"required":["kind","value"],"properties":{"kind":{"type":"string","enum":["text"]},"value":{"type":"string","minLength":1,"maxLength":200,"pattern":"^[^\\x00-\\x1F\\x7F]+$"}}}]},"SurfaceComponent":{"type":"object","description":"One element of a generative UI surface (RFC-0020 §1), in a flat list: `component` names a catalog entry (a PascalCase built-in primitive such as `Stack`, or a registry block type such as `callout`), `children` refer to other ids of the same surface, and `props` follow the entry's schema, which the store checks. Field names follow A2UI v0.9.","additionalProperties":false,"required":["id","component"],"properties":{"id":{"$ref":"#/$defs/SurfaceComponentId"},"component":{"type":"string","pattern":"^(?:[A-Z][A-Za-z0-9]{1,40}|[a-z][a-z0-9-]{1,40})$"},"children":{"type":"array","maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponentId"}},"props":{"type":"object","description":"Literal values, `{path}` bindings into the surface's data, or `{call}` named functions; validated by the store against the catalog, not by this contract.","additionalProperties":true},"visible":{"type":"object","description":"A visibility condition over the surface's data (`eq`, `ne`, `gt`, `lt`, `and`, `or`, `not`); validated by the store.","additionalProperties":true},"motion":{"type":"object","description":"Motion presets for this component (RFC-0024 §2): `reveal`, `hover`, `speed`, `delay`, `stagger`, `replay` and later `scroll`, `loop`; enums and clamped numbers only, validated by the store against the catalog. The renderer turns them into CSS; agents never write CSS.","additionalProperties":true}}},"SurfaceComponentId":{"type":"string","description":"A component's id, unique within its surface; `root` is the tree's root (RFC-0020 §1).","pattern":"^[a-z][a-z0-9-]{0,40}$"},"ThemeRef":{"type":"object","description":"Theme (a.k.a. Experience) applied by the renderer. Config is theme-defined and validated by the theme's own schema, not here.","additionalProperties":false,"required":["name"],"properties":{"name":{"$ref":"#/$defs/Slug"},"version":{"type":"string","maxLength":64},"config":{"type":"object","additionalProperties":true}}},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"},"TimeZone":{"type":"string","description":"IANA time zone name from the tz database (RFC-0008), e.g. Europe/Istanbul, America/Argentina/Buenos_Aires, Etc/GMT+3 or UTC. Wall-clock times a person enters are kept with their zone, so the instant is recomputed when governments change the rules. The pattern bounds the shape only; writers also check the name against the runtime's Intl zone list.","pattern":"^(?:UTC|[A-Z][A-Za-z_-]+(?:/[A-Za-z0-9_+-]+){1,2})$","maxLength":64}}}; const 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 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 schema86 = {"type":"object","description":"The site's design rules for people and agents (RFC-0021): Markdown that agents read with the theme's DESIGN.md before they build. Guidance only: it changes neither rendering nor permissions.","additionalProperties":false,"required":["markdown"],"properties":{"markdown":{"type":"string","minLength":1,"maxLength":24000}}}; +const schema124 = {"type":"string","description":"IANA time zone name from the tz database (RFC-0008), e.g. Europe/Istanbul, America/Argentina/Buenos_Aires, Etc/GMT+3 or UTC. Wall-clock times a person enters are kept with their zone, so the instant is recomputed when governments change the rules. The pattern bounds the shape only; writers also check the name against the runtime's Intl zone list.","pattern":"^(?:UTC|[A-Z][A-Za-z_-]+(?:/[A-Za-z0-9_+-]+){1,2})$","maxLength":64}; +const schema126 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; +const schema110 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema129 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; const 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 pattern59 = new RegExp("^(?:UTC|[A-Z][A-Za-z_-]+(?:/[A-Za-z0-9_+-]+){1,2})$", "u"); +const pattern66 = 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,13 +842,13 @@ 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"]},"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 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"}},"listFields":{"type":"array","description":"RFC-0042: fields that listings, queries, Studio columns and MCP may filter and sort by, in column order. Keys of this collection's fields (checked in code).","minItems":1,"maxItems":4,"uniqueItems":true,"items":{"$ref":"#/$defs/FieldKey"}},"expiry":{"$ref":"#/$defs/CollectionExpiry"},"structuredData":{"$ref":"#/$defs/StructuredDataPreset"}}}; 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 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 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 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 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}}}; @@ -3041,6 +3041,793 @@ return errors === 0; } validate25.evaluated = {"dynamicProps":true,"dynamicItems":false}; +const schema77 = {"type":"object","description":"RFC-0042: a time gate every entry derives from one of its date or datetime fields, checked at render time like Availability.until. A date ends at the start of the next day in the site's time zone.","additionalProperties":false,"required":["field","action"],"properties":{"field":{"$ref":"#/$defs/FieldKey"},"action":{"type":"string","description":"unlist: the page stays, the entry leaves listings, feeds, the sitemap and search. hide: every public route answers 404.","enum":["unlist","hide"]},"afterMinutes":{"type":"integer","description":"Grace after the field's instant. Absent: 0.","minimum":0,"maximum":10080}}}; + +function validate49(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +let vErrors = null; +let errors = 0; +const evaluated0 = validate49.evaluated; +if(evaluated0.dynamicProps){ +evaluated0.props = undefined; +} +if(evaluated0.dynamicItems){ +evaluated0.items = undefined; +} +if(data && typeof data == "object" && !Array.isArray(data)){ +if(data.field === undefined){ +const err0 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "field"},message:"must have required property '"+"field"+"'"}; +if(vErrors === null){ +vErrors = [err0]; +} +else { +vErrors.push(err0); +} +errors++; +} +if(data.action === undefined){ +const err1 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "action"},message:"must have required property '"+"action"+"'"}; +if(vErrors === null){ +vErrors = [err1]; +} +else { +vErrors.push(err1); +} +errors++; +} +for(const key0 in data){ +if(!(((key0 === "field") || (key0 === "action")) || (key0 === "afterMinutes"))){ +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.field !== undefined){ +let data0 = data.field; +if(typeof data0 === "string"){ +if(!pattern22.test(data0)){ +const err3 = {instancePath:instancePath+"/field",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 = [err3]; +} +else { +vErrors.push(err3); +} +errors++; +} +} +else { +const err4 = {instancePath:instancePath+"/field",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err4]; +} +else { +vErrors.push(err4); +} +errors++; +} +} +if(data.action !== undefined){ +let data1 = data.action; +if(typeof data1 !== "string"){ +const err5 = {instancePath:instancePath+"/action",schemaPath:"#/properties/action/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err5]; +} +else { +vErrors.push(err5); +} +errors++; +} +if(!((data1 === "unlist") || (data1 === "hide"))){ +const err6 = {instancePath:instancePath+"/action",schemaPath:"#/properties/action/enum",keyword:"enum",params:{allowedValues: schema77.properties.action.enum},message:"must be equal to one of the allowed values"}; +if(vErrors === null){ +vErrors = [err6]; +} +else { +vErrors.push(err6); +} +errors++; +} +} +if(data.afterMinutes !== undefined){ +let data2 = data.afterMinutes; +if(!(((typeof data2 == "number") && (!(data2 % 1) && !isNaN(data2))) && (isFinite(data2)))){ +const err7 = {instancePath:instancePath+"/afterMinutes",schemaPath:"#/properties/afterMinutes/type",keyword:"type",params:{type: "integer"},message:"must be integer"}; +if(vErrors === null){ +vErrors = [err7]; +} +else { +vErrors.push(err7); +} +errors++; +} +if((typeof data2 == "number") && (isFinite(data2))){ +if(data2 > 10080 || isNaN(data2)){ +const err8 = {instancePath:instancePath+"/afterMinutes",schemaPath:"#/properties/afterMinutes/maximum",keyword:"maximum",params:{comparison: "<=", limit: 10080},message:"must be <= 10080"}; +if(vErrors === null){ +vErrors = [err8]; +} +else { +vErrors.push(err8); +} +errors++; +} +if(data2 < 0 || isNaN(data2)){ +const err9 = {instancePath:instancePath+"/afterMinutes",schemaPath:"#/properties/afterMinutes/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"}; +if(vErrors === null){ +vErrors = [err9]; +} +else { +vErrors.push(err9); +} +errors++; +} +} +} +} +else { +const err10 = {instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err10]; +} +else { +vErrors.push(err10); +} +errors++; +} +validate49.errors = vErrors; +return errors === 0; +} +validate49.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; + +const schema79 = {"type":"object","description":"RFC-0042: the JSON-LD type an entry page emits in place of BlogPosting, with its properties mapped to value sources. Paths, fitting field types and required properties are a closed table in code (@cloudhal/core-ts/structured-data).","additionalProperties":false,"required":["type","properties"],"properties":{"type":{"type":"string","enum":["Event","JobPosting","Product","Recipe"]},"properties":{"type":"object","description":"Keyed by property path, e.g. startDate or location.address.addressLocality.","propertyNames":{"type":"string","pattern":"^[a-z][A-Za-z]{0,39}(\\.[a-z][A-Za-z]{0,39}){0,2}$"},"minProperties":1,"maxProperties":40,"additionalProperties":{"$ref":"#/$defs/StructuredDataSource"}}}}; +const pattern38 = new RegExp("^[a-z][A-Za-z]{0,39}(\\.[a-z][A-Za-z]{0,39}){0,2}$", "u"); +const schema80 = {"description":"RFC-0042: where one JSON-LD property's value comes from, discriminated by `kind`.","oneOf":[{"title":"FieldValueSource","type":"object","description":"A typed field of this collection, converted by its type.","additionalProperties":false,"required":["kind","field"],"properties":{"kind":{"type":"string","enum":["field"]},"field":{"$ref":"#/$defs/FieldKey"}}},{"title":"EntryValueSource","type":"object","description":"A built-in property of the entry.","additionalProperties":false,"required":["kind","property"],"properties":{"kind":{"type":"string","enum":["entry"]},"property":{"$ref":"#/$defs/EntryValueProperty"}}},{"title":"SiteValueSource","type":"object","description":"A property of the site.","additionalProperties":false,"required":["kind","property"],"properties":{"kind":{"type":"string","enum":["site"]},"property":{"$ref":"#/$defs/SiteValueProperty"}}},{"title":"TextValueSource","type":"object","description":"A plain-text constant: no control characters, no markup meaning; vocabulary-checked on enum paths (in code).","additionalProperties":false,"required":["kind","value"],"properties":{"kind":{"type":"string","enum":["text"]},"value":{"type":"string","minLength":1,"maxLength":200,"pattern":"^[^\\x00-\\x1F\\x7F]+$"}}}]}; +const schema82 = {"type":"string","description":"RFC-0042: entry properties a structured-data property may read.","enum":["title","summary","body","coverImage","url","publishedAt","updatedAt"]}; +const schema83 = {"type":"string","description":"RFC-0042: site properties a structured-data property may read.","enum":["name","url","logo"]}; +const pattern40 = new RegExp("^[^\\x00-\\x1F\\x7F]+$", "u"); + +function validate52(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +let vErrors = null; +let errors = 0; +const evaluated0 = validate52.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.kind === undefined){ +const err0 = {instancePath,schemaPath:"#/oneOf/0/required",keyword:"required",params:{missingProperty: "kind"},message:"must have required property '"+"kind"+"'"}; +if(vErrors === null){ +vErrors = [err0]; +} +else { +vErrors.push(err0); +} +errors++; +} +if(data.field === undefined){ +const err1 = {instancePath,schemaPath:"#/oneOf/0/required",keyword:"required",params:{missingProperty: "field"},message:"must have required property '"+"field"+"'"}; +if(vErrors === null){ +vErrors = [err1]; +} +else { +vErrors.push(err1); +} +errors++; +} +for(const key0 in data){ +if(!((key0 === "kind") || (key0 === "field"))){ +const err2 = {instancePath,schemaPath:"#/oneOf/0/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}; +if(vErrors === null){ +vErrors = [err2]; +} +else { +vErrors.push(err2); +} +errors++; +} +} +if(data.kind !== undefined){ +let data0 = data.kind; +if(typeof data0 !== "string"){ +const err3 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err3]; +} +else { +vErrors.push(err3); +} +errors++; +} +if(!(data0 === "field")){ +const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema80.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; +if(vErrors === null){ +vErrors = [err4]; +} +else { +vErrors.push(err4); +} +errors++; +} +} +if(data.field !== undefined){ +let data1 = data.field; +if(typeof data1 === "string"){ +if(!pattern22.test(data1)){ +const err5 = {instancePath:instancePath+"/field",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+"/field",schemaPath:"#/$defs/FieldKey/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err6]; +} +else { +vErrors.push(err6); +} +errors++; +} +} +} +else { +const err7 = {instancePath,schemaPath:"#/oneOf/0/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err7]; +} +else { +vErrors.push(err7); +} +errors++; +} +var _valid0 = _errs1 === errors; +if(_valid0){ +valid0 = true; +passing0 = 0; +var props0 = true; +} +const _errs9 = errors; +if(data && typeof data == "object" && !Array.isArray(data)){ +if(data.kind === undefined){ +const err8 = {instancePath,schemaPath:"#/oneOf/1/required",keyword:"required",params:{missingProperty: "kind"},message:"must have required property '"+"kind"+"'"}; +if(vErrors === null){ +vErrors = [err8]; +} +else { +vErrors.push(err8); +} +errors++; +} +if(data.property === undefined){ +const err9 = {instancePath,schemaPath:"#/oneOf/1/required",keyword:"required",params:{missingProperty: "property"},message:"must have required property '"+"property"+"'"}; +if(vErrors === null){ +vErrors = [err9]; +} +else { +vErrors.push(err9); +} +errors++; +} +for(const key1 in data){ +if(!((key1 === "kind") || (key1 === "property"))){ +const err10 = {instancePath,schemaPath:"#/oneOf/1/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key1},message:"must NOT have additional properties"}; +if(vErrors === null){ +vErrors = [err10]; +} +else { +vErrors.push(err10); +} +errors++; +} +} +if(data.kind !== undefined){ +let data2 = data.kind; +if(typeof data2 !== "string"){ +const err11 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err11]; +} +else { +vErrors.push(err11); +} +errors++; +} +if(!(data2 === "entry")){ +const err12 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema80.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; +if(vErrors === null){ +vErrors = [err12]; +} +else { +vErrors.push(err12); +} +errors++; +} +} +if(data.property !== undefined){ +let data3 = data.property; +if(typeof data3 !== "string"){ +const err13 = {instancePath:instancePath+"/property",schemaPath:"#/$defs/EntryValueProperty/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err13]; +} +else { +vErrors.push(err13); +} +errors++; +} +if(!(((((((data3 === "title") || (data3 === "summary")) || (data3 === "body")) || (data3 === "coverImage")) || (data3 === "url")) || (data3 === "publishedAt")) || (data3 === "updatedAt"))){ +const err14 = {instancePath:instancePath+"/property",schemaPath:"#/$defs/EntryValueProperty/enum",keyword:"enum",params:{allowedValues: schema82.enum},message:"must be equal to one of the allowed values"}; +if(vErrors === null){ +vErrors = [err14]; +} +else { +vErrors.push(err14); +} +errors++; +} +} +} +else { +const err15 = {instancePath,schemaPath:"#/oneOf/1/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err15]; +} +else { +vErrors.push(err15); +} +errors++; +} +var _valid0 = _errs9 === errors; +if(_valid0 && valid0){ +valid0 = false; +passing0 = [passing0, 1]; +} +else { +if(_valid0){ +valid0 = true; +passing0 = 1; +if(props0 !== true){ +props0 = true; +} +} +const _errs17 = errors; +if(data && typeof data == "object" && !Array.isArray(data)){ +if(data.kind === undefined){ +const err16 = {instancePath,schemaPath:"#/oneOf/2/required",keyword:"required",params:{missingProperty: "kind"},message:"must have required property '"+"kind"+"'"}; +if(vErrors === null){ +vErrors = [err16]; +} +else { +vErrors.push(err16); +} +errors++; +} +if(data.property === undefined){ +const err17 = {instancePath,schemaPath:"#/oneOf/2/required",keyword:"required",params:{missingProperty: "property"},message:"must have required property '"+"property"+"'"}; +if(vErrors === null){ +vErrors = [err17]; +} +else { +vErrors.push(err17); +} +errors++; +} +for(const key2 in data){ +if(!((key2 === "kind") || (key2 === "property"))){ +const err18 = {instancePath,schemaPath:"#/oneOf/2/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key2},message:"must NOT have additional properties"}; +if(vErrors === null){ +vErrors = [err18]; +} +else { +vErrors.push(err18); +} +errors++; +} +} +if(data.kind !== undefined){ +let data4 = data.kind; +if(typeof data4 !== "string"){ +const err19 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/2/properties/kind/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err19]; +} +else { +vErrors.push(err19); +} +errors++; +} +if(!(data4 === "site")){ +const err20 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/2/properties/kind/enum",keyword:"enum",params:{allowedValues: schema80.oneOf[2].properties.kind.enum},message:"must be equal to one of the allowed values"}; +if(vErrors === null){ +vErrors = [err20]; +} +else { +vErrors.push(err20); +} +errors++; +} +} +if(data.property !== undefined){ +let data5 = data.property; +if(typeof data5 !== "string"){ +const err21 = {instancePath:instancePath+"/property",schemaPath:"#/$defs/SiteValueProperty/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err21]; +} +else { +vErrors.push(err21); +} +errors++; +} +if(!(((data5 === "name") || (data5 === "url")) || (data5 === "logo"))){ +const err22 = {instancePath:instancePath+"/property",schemaPath:"#/$defs/SiteValueProperty/enum",keyword:"enum",params:{allowedValues: schema83.enum},message:"must be equal to one of the allowed values"}; +if(vErrors === null){ +vErrors = [err22]; +} +else { +vErrors.push(err22); +} +errors++; +} +} +} +else { +const err23 = {instancePath,schemaPath:"#/oneOf/2/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err23]; +} +else { +vErrors.push(err23); +} +errors++; +} +var _valid0 = _errs17 === errors; +if(_valid0 && valid0){ +valid0 = false; +passing0 = [passing0, 2]; +} +else { +if(_valid0){ +valid0 = true; +passing0 = 2; +if(props0 !== true){ +props0 = true; +} +} +const _errs25 = errors; +if(data && typeof data == "object" && !Array.isArray(data)){ +if(data.kind === undefined){ +const err24 = {instancePath,schemaPath:"#/oneOf/3/required",keyword:"required",params:{missingProperty: "kind"},message:"must have required property '"+"kind"+"'"}; +if(vErrors === null){ +vErrors = [err24]; +} +else { +vErrors.push(err24); +} +errors++; +} +if(data.value === undefined){ +const err25 = {instancePath,schemaPath:"#/oneOf/3/required",keyword:"required",params:{missingProperty: "value"},message:"must have required property '"+"value"+"'"}; +if(vErrors === null){ +vErrors = [err25]; +} +else { +vErrors.push(err25); +} +errors++; +} +for(const key3 in data){ +if(!((key3 === "kind") || (key3 === "value"))){ +const err26 = {instancePath,schemaPath:"#/oneOf/3/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key3},message:"must NOT have additional properties"}; +if(vErrors === null){ +vErrors = [err26]; +} +else { +vErrors.push(err26); +} +errors++; +} +} +if(data.kind !== undefined){ +let data6 = data.kind; +if(typeof data6 !== "string"){ +const err27 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/3/properties/kind/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err27]; +} +else { +vErrors.push(err27); +} +errors++; +} +if(!(data6 === "text")){ +const err28 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/3/properties/kind/enum",keyword:"enum",params:{allowedValues: schema80.oneOf[3].properties.kind.enum},message:"must be equal to one of the allowed values"}; +if(vErrors === null){ +vErrors = [err28]; +} +else { +vErrors.push(err28); +} +errors++; +} +} +if(data.value !== undefined){ +let data7 = data.value; +if(typeof data7 === "string"){ +if(func2(data7) > 200){ +const err29 = {instancePath:instancePath+"/value",schemaPath:"#/oneOf/3/properties/value/maxLength",keyword:"maxLength",params:{limit: 200},message:"must NOT have more than 200 characters"}; +if(vErrors === null){ +vErrors = [err29]; +} +else { +vErrors.push(err29); +} +errors++; +} +if(func2(data7) < 1){ +const err30 = {instancePath:instancePath+"/value",schemaPath:"#/oneOf/3/properties/value/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}; +if(vErrors === null){ +vErrors = [err30]; +} +else { +vErrors.push(err30); +} +errors++; +} +if(!pattern40.test(data7)){ +const err31 = {instancePath:instancePath+"/value",schemaPath:"#/oneOf/3/properties/value/pattern",keyword:"pattern",params:{pattern: "^[^\\x00-\\x1F\\x7F]+$"},message:"must match pattern \""+"^[^\\x00-\\x1F\\x7F]+$"+"\""}; +if(vErrors === null){ +vErrors = [err31]; +} +else { +vErrors.push(err31); +} +errors++; +} +} +else { +const err32 = {instancePath:instancePath+"/value",schemaPath:"#/oneOf/3/properties/value/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err32]; +} +else { +vErrors.push(err32); +} +errors++; +} +} +} +else { +const err33 = {instancePath,schemaPath:"#/oneOf/3/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err33]; +} +else { +vErrors.push(err33); +} +errors++; +} +var _valid0 = _errs25 === errors; +if(_valid0 && valid0){ +valid0 = false; +passing0 = [passing0, 3]; +} +else { +if(_valid0){ +valid0 = true; +passing0 = 3; +if(props0 !== true){ +props0 = true; +} +} +} +} +} +if(!valid0){ +const err34 = {instancePath,schemaPath:"#/oneOf",keyword:"oneOf",params:{passingSchemas: passing0},message:"must match exactly one schema in oneOf"}; +if(vErrors === null){ +vErrors = [err34]; +} +else { +vErrors.push(err34); +} +errors++; +} +else { +errors = _errs0; +if(vErrors !== null){ +if(_errs0){ +vErrors.length = _errs0; +} +else { +vErrors = null; +} +} +} +validate52.errors = vErrors; +evaluated0.props = props0; +return errors === 0; +} +validate52.evaluated = {"dynamicProps":true,"dynamicItems":false}; + + +function validate51(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +let vErrors = null; +let errors = 0; +const evaluated0 = validate51.evaluated; +if(evaluated0.dynamicProps){ +evaluated0.props = undefined; +} +if(evaluated0.dynamicItems){ +evaluated0.items = undefined; +} +if(data && typeof data == "object" && !Array.isArray(data)){ +if(data.type === undefined){ +const err0 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "type"},message:"must have required property '"+"type"+"'"}; +if(vErrors === null){ +vErrors = [err0]; +} +else { +vErrors.push(err0); +} +errors++; +} +if(data.properties === undefined){ +const err1 = {instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: "properties"},message:"must have required property '"+"properties"+"'"}; +if(vErrors === null){ +vErrors = [err1]; +} +else { +vErrors.push(err1); +} +errors++; +} +for(const key0 in data){ +if(!((key0 === "type") || (key0 === "properties"))){ +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.type !== undefined){ +let data0 = data.type; +if(typeof data0 !== "string"){ +const err3 = {instancePath:instancePath+"/type",schemaPath:"#/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err3]; +} +else { +vErrors.push(err3); +} +errors++; +} +if(!((((data0 === "Event") || (data0 === "JobPosting")) || (data0 === "Product")) || (data0 === "Recipe"))){ +const err4 = {instancePath:instancePath+"/type",schemaPath:"#/properties/type/enum",keyword:"enum",params:{allowedValues: schema79.properties.type.enum},message:"must be equal to one of the allowed values"}; +if(vErrors === null){ +vErrors = [err4]; +} +else { +vErrors.push(err4); +} +errors++; +} +} +if(data.properties !== undefined){ +let data1 = data.properties; +if(data1 && typeof data1 == "object" && !Array.isArray(data1)){ +if(Object.keys(data1).length > 40){ +const err5 = {instancePath:instancePath+"/properties",schemaPath:"#/properties/properties/maxProperties",keyword:"maxProperties",params:{limit: 40},message:"must NOT have more than 40 properties"}; +if(vErrors === null){ +vErrors = [err5]; +} +else { +vErrors.push(err5); +} +errors++; +} +if(Object.keys(data1).length < 1){ +const err6 = {instancePath:instancePath+"/properties",schemaPath:"#/properties/properties/minProperties",keyword:"minProperties",params:{limit: 1},message:"must NOT have fewer than 1 properties"}; +if(vErrors === null){ +vErrors = [err6]; +} +else { +vErrors.push(err6); +} +errors++; +} +for(const key1 in data1){ +const _errs6 = errors; +if(typeof key1 === "string"){ +if(!pattern38.test(key1)){ +const err7 = {instancePath:instancePath+"/properties",schemaPath:"#/properties/properties/propertyNames/pattern",keyword:"pattern",params:{pattern: "^[a-z][A-Za-z]{0,39}(\\.[a-z][A-Za-z]{0,39}){0,2}$"},message:"must match pattern \""+"^[a-z][A-Za-z]{0,39}(\\.[a-z][A-Za-z]{0,39}){0,2}$"+"\"",propertyName:key1}; +if(vErrors === null){ +vErrors = [err7]; +} +else { +vErrors.push(err7); +} +errors++; +} +} +else { +const err8 = {instancePath:instancePath+"/properties",schemaPath:"#/properties/properties/propertyNames/type",keyword:"type",params:{type: "string"},message:"must be string",propertyName:key1}; +if(vErrors === null){ +vErrors = [err8]; +} +else { +vErrors.push(err8); +} +errors++; +} +var valid1 = _errs6 === errors; +if(!valid1){ +const err9 = {instancePath:instancePath+"/properties",schemaPath:"#/properties/properties/propertyNames",keyword:"propertyNames",params:{propertyName: key1},message:"property name must be valid"}; +if(vErrors === null){ +vErrors = [err9]; +} +else { +vErrors.push(err9); +} +errors++; +} +} +for(const key2 in data1){ +if(!(validate52(data1[key2], {instancePath:instancePath+"/properties/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),parentData:data1,parentDataProperty:key2,rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate52.errors : vErrors.concat(validate52.errors); +errors = vErrors.length; +} +} +} +else { +const err10 = {instancePath:instancePath+"/properties",schemaPath:"#/properties/properties/type",keyword:"type",params:{type: "object"},message:"must be object"}; +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++; +} +validate51.errors = vErrors; +return errors === 0; +} +validate51.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; + function validate24(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; @@ -3054,7 +3841,7 @@ evaluated0.items = undefined; } if(data && typeof data == "object" && !Array.isArray(data)){ for(const key0 in data){ -if(!(((((((key0 === "permalink") || (key0 === "labels")) || (key0 === "icon")) || (key0 === "feed")) || (key0 === "sitemap")) || (key0 === "sort")) || (key0 === "fields"))){ +if(!(func1.call(schema54.properties, key0))){ const err0 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}; if(vErrors === null){ vErrors = [err0]; @@ -3417,9 +4204,11 @@ vErrors.push(err29); errors++; } } -} -else { -const err30 = {instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(data.listFields !== undefined){ +let data12 = data.listFields; +if(Array.isArray(data12)){ +if(data12.length > 4){ +const err30 = {instancePath:instancePath+"/listFields",schemaPath:"#/properties/listFields/maxItems",keyword:"maxItems",params:{limit: 4},message:"must NOT have more than 4 items"}; if(vErrors === null){ vErrors = [err30]; } @@ -3428,17 +4217,108 @@ vErrors.push(err30); } errors++; } +if(data12.length < 1){ +const err31 = {instancePath:instancePath+"/listFields",schemaPath:"#/properties/listFields/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}; +if(vErrors === null){ +vErrors = [err31]; +} +else { +vErrors.push(err31); +} +errors++; +} +const len1 = data12.length; +for(let i1=0; i1 1){ +outer0: +for(;i2--;){ +for(j0 = i2; j0--;){ +if(func0(data12[i2], data12[j0])){ +const err34 = {instancePath:instancePath+"/listFields",schemaPath:"#/properties/listFields/uniqueItems",keyword:"uniqueItems",params:{i: i2, j: j0},message:"must NOT have duplicate items (items ## "+j0+" and "+i2+" are identical)"}; +if(vErrors === null){ +vErrors = [err34]; +} +else { +vErrors.push(err34); +} +errors++; +break outer0; +} +} +} +} +} +else { +const err35 = {instancePath:instancePath+"/listFields",schemaPath:"#/properties/listFields/type",keyword:"type",params:{type: "array"},message:"must be array"}; +if(vErrors === null){ +vErrors = [err35]; +} +else { +vErrors.push(err35); +} +errors++; +} +} +if(data.expiry !== undefined){ +if(!(validate49(data.expiry, {instancePath:instancePath+"/expiry",parentData:data,parentDataProperty:"expiry",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate49.errors : vErrors.concat(validate49.errors); +errors = vErrors.length; +} +} +if(data.structuredData !== undefined){ +if(!(validate51(data.structuredData, {instancePath:instancePath+"/structuredData",parentData:data,parentDataProperty:"structuredData",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate51.errors : vErrors.concat(validate51.errors); +errors = vErrors.length; +} +} +} +else { +const err36 = {instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err36]; +} +else { +vErrors.push(err36); +} +errors++; +} validate24.errors = vErrors; return errors === 0; } validate24.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -const schema76 = {"type":"object","description":"Theme (a.k.a. Experience) applied by the renderer. Config is theme-defined and validated by the theme's own schema, not here.","additionalProperties":false,"required":["name"],"properties":{"name":{"$ref":"#/$defs/Slug"},"version":{"type":"string","maxLength":64},"config":{"type":"object","additionalProperties":true}}}; +const schema84 = {"type":"object","description":"Theme (a.k.a. Experience) applied by the renderer. Config is theme-defined and validated by the theme's own schema, not here.","additionalProperties":false,"required":["name"],"properties":{"name":{"$ref":"#/$defs/Slug"},"version":{"type":"string","maxLength":64},"config":{"type":"object","additionalProperties":true}}}; -function validate50(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate56(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate50.evaluated; +const evaluated0 = validate56.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -3564,19 +4444,19 @@ vErrors.push(err9); } errors++; } -validate50.errors = vErrors; +validate56.errors = vErrors; return errors === 0; } -validate50.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate56.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -const schema79 = {"type":"object","description":"The site's logo and social links (RFC-0025 §1), shared by the header, the footer and SEO.","additionalProperties":false,"properties":{"logo":{"$ref":"#/$defs/ImageRef"},"logoDark":{"$ref":"#/$defs/ImageRef"},"logoHeight":{"type":"string","enum":["s","m","l"]},"social":{"type":"array","maxItems":8,"items":{"$ref":"#/$defs/SocialLink"}}}}; -const schema82 = {"type":"object","additionalProperties":false,"required":["network","href"],"properties":{"network":{"type":"string","enum":["x","bluesky","mastodon","github","linkedin","instagram","facebook","youtube","tiktok","threads","rss","email"]},"href":{"type":"string","minLength":1,"maxLength":2048}}}; -const schema80 = {"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}}; +const schema87 = {"type":"object","description":"The site's logo and social links (RFC-0025 §1), shared by the header, the footer and SEO.","additionalProperties":false,"properties":{"logo":{"$ref":"#/$defs/ImageRef"},"logoDark":{"$ref":"#/$defs/ImageRef"},"logoHeight":{"type":"string","enum":["s","m","l"]},"social":{"type":"array","maxItems":8,"items":{"$ref":"#/$defs/SocialLink"}}}}; +const schema90 = {"type":"object","additionalProperties":false,"required":["network","href"],"properties":{"network":{"type":"string","enum":["x","bluesky","mastodon","github","linkedin","instagram","facebook","youtube","tiktok","threads","rss","email"]},"href":{"type":"string","minLength":1,"maxLength":2048}}}; +const schema88 = {"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}}; -function validate53(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate59(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate53.evaluated; +const evaluated0 = validate59.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -3782,16 +4662,16 @@ vErrors.push(err16); } errors++; } -validate53.errors = vErrors; +validate59.errors = vErrors; return errors === 0; } -validate53.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate59.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate52(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate58(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate52.evaluated; +const evaluated0 = validate58.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -3812,14 +4692,14 @@ errors++; } } if(data.logo !== undefined){ -if(!(validate53(data.logo, {instancePath:instancePath+"/logo",parentData:data,parentDataProperty:"logo",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate53.errors : vErrors.concat(validate53.errors); +if(!(validate59(data.logo, {instancePath:instancePath+"/logo",parentData:data,parentDataProperty:"logo",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate59.errors : vErrors.concat(validate59.errors); errors = vErrors.length; } } if(data.logoDark !== undefined){ -if(!(validate53(data.logoDark, {instancePath:instancePath+"/logoDark",parentData:data,parentDataProperty:"logoDark",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate53.errors : vErrors.concat(validate53.errors); +if(!(validate59(data.logoDark, {instancePath:instancePath+"/logoDark",parentData:data,parentDataProperty:"logoDark",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate59.errors : vErrors.concat(validate59.errors); errors = vErrors.length; } } @@ -3836,7 +4716,7 @@ vErrors.push(err1); errors++; } if(!(((data2 === "s") || (data2 === "m")) || (data2 === "l"))){ -const err2 = {instancePath:instancePath+"/logoHeight",schemaPath:"#/properties/logoHeight/enum",keyword:"enum",params:{allowedValues: schema79.properties.logoHeight.enum},message:"must be equal to one of the allowed values"}; +const err2 = {instancePath:instancePath+"/logoHeight",schemaPath:"#/properties/logoHeight/enum",keyword:"enum",params:{allowedValues: schema87.properties.logoHeight.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err2]; } @@ -3908,7 +4788,7 @@ vErrors.push(err7); errors++; } if(!((((((((((((data5 === "x") || (data5 === "bluesky")) || (data5 === "mastodon")) || (data5 === "github")) || (data5 === "linkedin")) || (data5 === "instagram")) || (data5 === "facebook")) || (data5 === "youtube")) || (data5 === "tiktok")) || (data5 === "threads")) || (data5 === "rss")) || (data5 === "email"))){ -const err8 = {instancePath:instancePath+"/social/" + i0+"/network",schemaPath:"#/$defs/SocialLink/properties/network/enum",keyword:"enum",params:{allowedValues: schema82.properties.network.enum},message:"must be equal to one of the allowed values"}; +const err8 = {instancePath:instancePath+"/social/" + i0+"/network",schemaPath:"#/$defs/SocialLink/properties/network/enum",keyword:"enum",params:{allowedValues: schema90.properties.network.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err8]; } @@ -3988,20 +4868,20 @@ vErrors.push(err14); } errors++; } -validate52.errors = vErrors; +validate58.errors = vErrors; return errors === 0; } -validate52.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate58.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -const schema83 = {"type":"object","description":"The site's header, footer and bars as data (RFC-0025): enums, menu keys and short texts; the theme renders them with landmarks, focus and contrast guaranteed.","additionalProperties":false,"properties":{"header":{"$ref":"#/$defs/ChromeHeader"},"footer":{"$ref":"#/$defs/ChromeFooter"},"announcement":{"$ref":"#/$defs/Announcement"},"consent":{"$ref":"#/$defs/Consent"}}}; -const schema111 = {"type":"object","additionalProperties":false,"required":["id","text"],"properties":{"id":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,40}$","description":"Change it when the text changes, so readers who dismissed the old one see the new one."},"text":{"type":"string","minLength":1,"maxLength":200},"label":{"type":"string","minLength":1,"maxLength":40},"href":{"type":"string","minLength":1,"maxLength":2048},"tone":{"type":"string","enum":["info","brand","warning"]},"dismissible":{"type":"boolean"}}}; -const schema84 = {"type":"object","additionalProperties":false,"properties":{"layout":{"type":"string","enum":["logo-left","logo-left-nav-center","centered","split","minimal"]},"behavior":{"type":"string","enum":["static","sticky","hide-on-scroll","fixed"]},"shrink":{"type":"boolean"},"overHero":{"type":"string","enum":["solid","transparent"]},"width":{"type":"string","enum":["content","wide","full"]},"surface":{"type":"string","enum":["plain","muted","raised","inverse","brand"]},"mobileStyle":{"type":"string","enum":["dropdown","drawer-start","drawer-end","fullscreen"]},"collapseAt":{"type":"string","enum":["sm","md","lg","always"]},"nav":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."},"utility":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."},"actions":{"type":"array","maxItems":3,"items":{"$ref":"#/$defs/HeaderAction"}}}}; -const schema87 = {"type":"object","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["button","search","locale","theme-toggle","motion-toggle"]},"label":{"type":"string","minLength":1,"maxLength":40},"href":{"type":"string","minLength":1,"maxLength":2048},"variant":{"type":"string","enum":["primary","secondary"]}}}; +const schema91 = {"type":"object","description":"The site's header, footer and bars as data (RFC-0025): enums, menu keys and short texts; the theme renders them with landmarks, focus and contrast guaranteed.","additionalProperties":false,"properties":{"header":{"$ref":"#/$defs/ChromeHeader"},"footer":{"$ref":"#/$defs/ChromeFooter"},"announcement":{"$ref":"#/$defs/Announcement"},"consent":{"$ref":"#/$defs/Consent"}}}; +const schema121 = {"type":"object","additionalProperties":false,"required":["id","text"],"properties":{"id":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,40}$","description":"Change it when the text changes, so readers who dismissed the old one see the new one."},"text":{"type":"string","minLength":1,"maxLength":200},"label":{"type":"string","minLength":1,"maxLength":40},"href":{"type":"string","minLength":1,"maxLength":2048},"tone":{"type":"string","enum":["info","brand","warning"]},"dismissible":{"type":"boolean"}}}; +const schema92 = {"type":"object","additionalProperties":false,"properties":{"layout":{"type":"string","enum":["logo-left","logo-left-nav-center","centered","split","minimal"]},"behavior":{"type":"string","enum":["static","sticky","hide-on-scroll","fixed"]},"shrink":{"type":"boolean"},"overHero":{"type":"string","enum":["solid","transparent"]},"width":{"type":"string","enum":["content","wide","full"]},"surface":{"type":"string","enum":["plain","muted","raised","inverse","brand"]},"mobileStyle":{"type":"string","enum":["dropdown","drawer-start","drawer-end","fullscreen"]},"collapseAt":{"type":"string","enum":["sm","md","lg","always"]},"nav":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."},"utility":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."},"actions":{"type":"array","maxItems":3,"items":{"$ref":"#/$defs/HeaderAction"}}}}; +const schema95 = {"type":"object","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["button","search","locale","theme-toggle","motion-toggle"]},"label":{"type":"string","minLength":1,"maxLength":40},"href":{"type":"string","minLength":1,"maxLength":2048},"variant":{"type":"string","enum":["primary","secondary"]}}}; -function validate58(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate64(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate58.evaluated; +const evaluated0 = validate64.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -4010,7 +4890,7 @@ evaluated0.items = undefined; } if(data && typeof data == "object" && !Array.isArray(data)){ for(const key0 in data){ -if(!(func1.call(schema84.properties, key0))){ +if(!(func1.call(schema92.properties, key0))){ const err0 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}; if(vErrors === null){ vErrors = [err0]; @@ -4034,7 +4914,7 @@ vErrors.push(err1); errors++; } if(!(((((data0 === "logo-left") || (data0 === "logo-left-nav-center")) || (data0 === "centered")) || (data0 === "split")) || (data0 === "minimal"))){ -const err2 = {instancePath:instancePath+"/layout",schemaPath:"#/properties/layout/enum",keyword:"enum",params:{allowedValues: schema84.properties.layout.enum},message:"must be equal to one of the allowed values"}; +const err2 = {instancePath:instancePath+"/layout",schemaPath:"#/properties/layout/enum",keyword:"enum",params:{allowedValues: schema92.properties.layout.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err2]; } @@ -4057,7 +4937,7 @@ vErrors.push(err3); errors++; } if(!((((data1 === "static") || (data1 === "sticky")) || (data1 === "hide-on-scroll")) || (data1 === "fixed"))){ -const err4 = {instancePath:instancePath+"/behavior",schemaPath:"#/properties/behavior/enum",keyword:"enum",params:{allowedValues: schema84.properties.behavior.enum},message:"must be equal to one of the allowed values"}; +const err4 = {instancePath:instancePath+"/behavior",schemaPath:"#/properties/behavior/enum",keyword:"enum",params:{allowedValues: schema92.properties.behavior.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -4092,7 +4972,7 @@ vErrors.push(err6); errors++; } if(!((data3 === "solid") || (data3 === "transparent"))){ -const err7 = {instancePath:instancePath+"/overHero",schemaPath:"#/properties/overHero/enum",keyword:"enum",params:{allowedValues: schema84.properties.overHero.enum},message:"must be equal to one of the allowed values"}; +const err7 = {instancePath:instancePath+"/overHero",schemaPath:"#/properties/overHero/enum",keyword:"enum",params:{allowedValues: schema92.properties.overHero.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err7]; } @@ -4115,7 +4995,7 @@ vErrors.push(err8); errors++; } if(!(((data4 === "content") || (data4 === "wide")) || (data4 === "full"))){ -const err9 = {instancePath:instancePath+"/width",schemaPath:"#/properties/width/enum",keyword:"enum",params:{allowedValues: schema84.properties.width.enum},message:"must be equal to one of the allowed values"}; +const err9 = {instancePath:instancePath+"/width",schemaPath:"#/properties/width/enum",keyword:"enum",params:{allowedValues: schema92.properties.width.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err9]; } @@ -4138,7 +5018,7 @@ vErrors.push(err10); errors++; } if(!(((((data5 === "plain") || (data5 === "muted")) || (data5 === "raised")) || (data5 === "inverse")) || (data5 === "brand"))){ -const err11 = {instancePath:instancePath+"/surface",schemaPath:"#/properties/surface/enum",keyword:"enum",params:{allowedValues: schema84.properties.surface.enum},message:"must be equal to one of the allowed values"}; +const err11 = {instancePath:instancePath+"/surface",schemaPath:"#/properties/surface/enum",keyword:"enum",params:{allowedValues: schema92.properties.surface.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err11]; } @@ -4161,7 +5041,7 @@ vErrors.push(err12); errors++; } if(!((((data6 === "dropdown") || (data6 === "drawer-start")) || (data6 === "drawer-end")) || (data6 === "fullscreen"))){ -const err13 = {instancePath:instancePath+"/mobileStyle",schemaPath:"#/properties/mobileStyle/enum",keyword:"enum",params:{allowedValues: schema84.properties.mobileStyle.enum},message:"must be equal to one of the allowed values"}; +const err13 = {instancePath:instancePath+"/mobileStyle",schemaPath:"#/properties/mobileStyle/enum",keyword:"enum",params:{allowedValues: schema92.properties.mobileStyle.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err13]; } @@ -4184,7 +5064,7 @@ vErrors.push(err14); errors++; } if(!((((data7 === "sm") || (data7 === "md")) || (data7 === "lg")) || (data7 === "always"))){ -const err15 = {instancePath:instancePath+"/collapseAt",schemaPath:"#/properties/collapseAt/enum",keyword:"enum",params:{allowedValues: schema84.properties.collapseAt.enum},message:"must be equal to one of the allowed values"}; +const err15 = {instancePath:instancePath+"/collapseAt",schemaPath:"#/properties/collapseAt/enum",keyword:"enum",params:{allowedValues: schema92.properties.collapseAt.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err15]; } @@ -4336,7 +5216,7 @@ vErrors.push(err27); errors++; } if(!(((((data12 === "button") || (data12 === "search")) || (data12 === "locale")) || (data12 === "theme-toggle")) || (data12 === "motion-toggle"))){ -const err28 = {instancePath:instancePath+"/actions/" + i0+"/kind",schemaPath:"#/$defs/HeaderAction/properties/kind/enum",keyword:"enum",params:{allowedValues: schema87.properties.kind.enum},message:"must be equal to one of the allowed values"}; +const err28 = {instancePath:instancePath+"/actions/" + i0+"/kind",schemaPath:"#/$defs/HeaderAction/properties/kind/enum",keyword:"enum",params:{allowedValues: schema95.properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err28]; } @@ -4429,7 +5309,7 @@ vErrors.push(err35); errors++; } if(!((data15 === "primary") || (data15 === "secondary"))){ -const err36 = {instancePath:instancePath+"/actions/" + i0+"/variant",schemaPath:"#/$defs/HeaderAction/properties/variant/enum",keyword:"enum",params:{allowedValues: schema87.properties.variant.enum},message:"must be equal to one of the allowed values"}; +const err36 = {instancePath:instancePath+"/actions/" + i0+"/variant",schemaPath:"#/$defs/HeaderAction/properties/variant/enum",keyword:"enum",params:{allowedValues: schema95.properties.variant.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err36]; } @@ -4474,18 +5354,18 @@ vErrors.push(err39); } errors++; } -validate58.errors = vErrors; +validate64.errors = vErrors; return errors === 0; } -validate58.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate64.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -const schema88 = {"type":"object","additionalProperties":false,"properties":{"layout":{"type":"string","enum":["simple","columns","cta-band","minimal"]},"surface":{"type":"string","enum":["plain","muted","inverse"]},"tagline":{"type":"string","minLength":1,"maxLength":200},"columns":{"type":"array","maxItems":5,"items":{"$ref":"#/$defs/FooterColumn"}},"ctaHeading":{"type":"string","minLength":1,"maxLength":120},"ctaLabel":{"type":"string","minLength":1,"maxLength":40},"ctaHref":{"type":"string","minLength":1,"maxLength":2048},"social":{"type":"boolean"},"copyright":{"type":"string","minLength":1,"maxLength":200,"description":"The legal line; {year} and {site} are the only placeholders."},"legal":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."},"extras":{"type":"array","maxItems":3,"uniqueItems":true,"items":{"type":"string","enum":["locale","theme-toggle","motion-toggle"]}},"credit":{"type":"boolean"},"region":{"$ref":"#/$defs/Section","description":"RFC-0025 §2: one optional surface (kind `surface`) above the footer's columns, built and checked like any surface; the store refuses other section kinds."}}}; -const schema89 = {"type":"object","additionalProperties":false,"required":["heading","menu"],"properties":{"heading":{"type":"string","minLength":1,"maxLength":60},"menu":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."}}}; +const schema96 = {"type":"object","additionalProperties":false,"properties":{"layout":{"type":"string","enum":["simple","columns","cta-band","minimal"]},"surface":{"type":"string","enum":["plain","muted","inverse"]},"tagline":{"type":"string","minLength":1,"maxLength":200},"columns":{"type":"array","maxItems":5,"items":{"$ref":"#/$defs/FooterColumn"}},"ctaHeading":{"type":"string","minLength":1,"maxLength":120},"ctaLabel":{"type":"string","minLength":1,"maxLength":40},"ctaHref":{"type":"string","minLength":1,"maxLength":2048},"social":{"type":"boolean"},"copyright":{"type":"string","minLength":1,"maxLength":200,"description":"The legal line; {year} and {site} are the only placeholders."},"legal":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."},"extras":{"type":"array","maxItems":3,"uniqueItems":true,"items":{"type":"string","enum":["locale","theme-toggle","motion-toggle"]}},"credit":{"type":"boolean"},"region":{"$ref":"#/$defs/Section","description":"RFC-0025 §2: one optional surface (kind `surface`) above the footer's columns, built and checked like any surface; the store refuses other section kinds."}}}; +const schema97 = {"type":"object","additionalProperties":false,"required":["heading","menu"],"properties":{"heading":{"type":"string","minLength":1,"maxLength":60},"menu":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."}}}; -function validate61(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate67(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate61.evaluated; +const evaluated0 = validate67.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -4616,27 +5496,29 @@ vErrors.push(err10); } errors++; } -validate61.errors = vErrors; +validate67.errors = vErrors; return errors === 0; } -validate61.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate67.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -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"]}; +const schema100 = {"title":"Section","description":"A block inside a page, discriminated by `kind`. Variants are written inline (not via $ref) and `kind` is a single-value string enum so that TypeScript gets a literal union and Rust gets a serde internally-tagged enum.","oneOf":[{"type":"object","title":"HeroSection","additionalProperties":false,"required":["id","kind","heading"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["hero"]},"heading":{"type":"string","minLength":1,"maxLength":300},"subheading":{"type":"string","maxLength":1000},"image":{"$ref":"#/$defs/ImageRef"},"cta":{"$ref":"#/$defs/Link"}}},{"type":"object","title":"RichTextSection","additionalProperties":false,"required":["id","kind","format","body"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["richText"]},"format":{"type":"string","enum":["markdown","html"]},"body":{"type":"string","maxLength":200000},"profile":{"$ref":"#/$defs/BodyProfile"},"blocks":{"$ref":"#/$defs/BodyBlocks"}}},{"type":"object","title":"ImageSection","additionalProperties":false,"required":["id","kind","image"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["image"]},"image":{"$ref":"#/$defs/ImageRef"},"caption":{"type":"string","maxLength":1000}}},{"type":"object","title":"ContentListSection","description":"Renders the newest entries of a content collection (e.g. blog posts).","additionalProperties":false,"required":["id","kind","collection","limit","sort"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["contentList"]},"collection":{"$ref":"#/$defs/Slug"},"limit":{"type":"integer","minimum":1,"maximum":100},"sort":{"type":"string","enum":["newest","oldest","title"]},"filter":{"type":"string","description":"Filter expression over term assignments (RFC-0002), e.g. 'category:news+tag:ai' or 'category:news>*'.","minLength":1,"maxLength":512},"where":{"type":"array","description":"RFC-0042: conditions on the collection's list fields, ANDed.","minItems":1,"maxItems":4,"items":{"$ref":"#/$defs/FieldCondition"}},"orderBy":{"$ref":"#/$defs/FieldOrder"}}},{"type":"object","title":"ComponentSection","description":"A typed block from the block registry (RFC-0015): `type` names a registered block type (one global namespace of English names), `props` follow that type's props schema, which the store validates.","additionalProperties":false,"required":["id","kind","type","props"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["component"]},"type":{"type":"string","pattern":"^[a-z][a-z0-9-]{1,40}$"},"props":{"type":"object","description":"Validated against the type's props schema by the store, not by this contract.","additionalProperties":true}}},{"type":"object","title":"SurfaceSection","description":"A generative UI surface (RFC-0020): a flat list of catalog components with a `root`, plus a data model their props may bind to. Written and streamed by agents as data, never code; the store checks the tree, the catalog and the limits.","additionalProperties":false,"required":["id","kind","catalog","components"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["surface"]},"catalog":{"type":"string","description":"The catalog the surface was written for: `@` (RFC-0018 manifest).","pattern":"^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"},"components":{"type":"array","minItems":1,"maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponent"}},"data":{"type":"object","description":"The surface's data model: JSON its components bind to with `{path}` (at most 64 KB serialized; the store enforces the size).","additionalProperties":true}}}]}; +const schema102 = {"type":"object","additionalProperties":false,"required":["label","href"],"properties":{"label":{"type":"string","minLength":1,"maxLength":200},"href":{"type":"string","minLength":1,"maxLength":2048},"external":{"type":"boolean"}}}; +const schema104 = {"type":"integer","enum":[1],"description":"The markdown dialect a body follows (RFC-0014 §1): 1 = Cloudhal Markdown v1 (CommonMark + GFM + `asset:` + directives; raw HTML escaped). Set by the store on markdown bodies."}; +const schema114 = {"type":"object","description":"RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`.","additionalProperties":false,"required":["field","op"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","in","set","unset"]},"value":{"type":"string","minLength":1,"maxLength":100},"values":{"type":"array","minItems":1,"maxItems":20,"uniqueItems":true,"items":{"type":"string","minLength":1,"maxLength":100}}}}; +const schema115 = {"type":"object","description":"RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last.","additionalProperties":false,"required":["field","direction"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"direction":{"type":"string","enum":["asc","desc"]}}}; +const pattern58 = new RegExp("^[a-z][a-z0-9-]{1,40}$", "u"); +const pattern60 = new RegExp("^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$", "u"); +const schema105 = {"type":"array","description":"A markdown body's block index, in body order (RFC-0014 §3). Absent means not indexed (yet, or an HTML body), never empty.","maxItems":5000,"items":{"$ref":"#/$defs/BodyBlock"}}; +const schema106 = {"type":"object","description":"One top-level block of a markdown body in the store's index (RFC-0014 §3). Written by the store only: a client's values are replaced on every write. `by`, `via` and `at` say who last changed the block's content, through which channel and when; they are absent for content written before the index existed.","additionalProperties":false,"required":["id","hash"],"properties":{"id":{"type":"string","pattern":"^blk_[0-9A-Za-z]{8,24}$","description":"Stable across writes that keep the block's content or its place among unchanged blocks."},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"SHA-256 (hex) of the block's markdown without trailing whitespace."},"by":{"$ref":"#/$defs/Actor"},"via":{"type":"string","enum":["studio","mcp","api","import","ai-command","proposal"],"description":"The channel of that change."},"at":{"$ref":"#/$defs/Timestamp"}}}; +const pattern49 = new RegExp("^blk_[0-9A-Za-z]{8,24}$", "u"); +const pattern50 = new RegExp("^[0-9a-f]{64}$", "u"); +const schema107 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; +const schema108 = {"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]}; -function validate67(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate73(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate67.evaluated; +const evaluated0 = validate73.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -4689,7 +5571,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: schema100.enum},message:"must be equal to one of the allowed values"}; +const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema108.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -4780,16 +5662,16 @@ vErrors.push(err11); } errors++; } -validate67.errors = vErrors; +validate73.errors = vErrors; return errors === 0; } -validate67.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate73.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate66(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate72(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate66.evaluated; +const evaluated0 = validate72.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -4832,7 +5714,7 @@ errors++; if(data.id !== undefined){ let data0 = data.id; if(typeof data0 === "string"){ -if(!pattern44.test(data0)){ +if(!pattern49.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]; @@ -4857,7 +5739,7 @@ errors++; if(data.hash !== undefined){ let data1 = data.hash; if(typeof data1 === "string"){ -if(!pattern45.test(data1)){ +if(!pattern50.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]; @@ -4880,8 +5762,8 @@ errors++; } } if(data.by !== undefined){ -if(!(validate67(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate67.errors : vErrors.concat(validate67.errors); +if(!(validate73(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate73.errors : vErrors.concat(validate73.errors); errors = vErrors.length; } } @@ -4898,7 +5780,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: schema98.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: schema106.properties.via.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err8]; } @@ -4944,16 +5826,16 @@ vErrors.push(err11); } errors++; } -validate66.errors = vErrors; +validate72.errors = vErrors; return errors === 0; } -validate66.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate72.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate65(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate71(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate65.evaluated; +const evaluated0 = validate71.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -4973,8 +5855,8 @@ errors++; } const len0 = data.length; for(let i0=0; i0 4){ +const err82 = {instancePath:instancePath+"/where",schemaPath:"#/oneOf/3/properties/where/maxItems",keyword:"maxItems",params:{limit: 4},message:"must NOT have more than 4 items"}; if(vErrors === null){ vErrors = [err82]; } @@ -6213,23 +7097,8 @@ vErrors.push(err82); } errors++; } -var _valid0 = _errs51 === errors; -if(_valid0 && valid0){ -valid0 = false; -passing0 = [passing0, 3]; -} -else { -if(_valid0){ -valid0 = true; -passing0 = 3; -if(props0 !== true){ -props0 = true; -} -} -const _errs68 = errors; -if(data && typeof data == "object" && !Array.isArray(data)){ -if(data.id === undefined){ -const err83 = {instancePath,schemaPath:"#/oneOf/4/required",keyword:"required",params:{missingProperty: "id"},message:"must have required property '"+"id"+"'"}; +if(data25.length < 1){ +const err83 = {instancePath:instancePath+"/where",schemaPath:"#/oneOf/3/properties/where/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}; if(vErrors === null){ vErrors = [err83]; } @@ -6238,8 +7107,12 @@ vErrors.push(err83); } errors++; } -if(data.kind === undefined){ -const err84 = {instancePath,schemaPath:"#/oneOf/4/required",keyword:"required",params:{missingProperty: "kind"},message:"must have required property '"+"kind"+"'"}; +const len0 = data25.length; +for(let i0=0; i0 48){ -const err88 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/maxLength",keyword:"maxLength",params:{limit: 48},message:"must NOT have more than 48 characters"}; +else { +const err88 = {instancePath:instancePath+"/where/" + i0+"/field",schemaPath:"#/$defs/FieldCondition/properties/field/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ vErrors = [err88]; } @@ -6293,8 +7167,11 @@ vErrors.push(err88); } errors++; } -if(func2(data25) < 8){ -const err89 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/minLength",keyword:"minLength",params:{limit: 8},message:"must NOT have fewer than 8 characters"}; +} +if(data26.op !== undefined){ +let data28 = data26.op; +if(typeof data28 !== "string"){ +const err89 = {instancePath:instancePath+"/where/" + i0+"/op",schemaPath:"#/$defs/FieldCondition/properties/op/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ vErrors = [err89]; } @@ -6303,8 +7180,8 @@ vErrors.push(err89); } errors++; } -if(!pattern4.test(data25)){ -const err90 = {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(!(((((((((data28 === "eq") || (data28 === "ne")) || (data28 === "lt")) || (data28 === "lte")) || (data28 === "gt")) || (data28 === "gte")) || (data28 === "in")) || (data28 === "set")) || (data28 === "unset"))){ +const err90 = {instancePath:instancePath+"/where/" + i0+"/op",schemaPath:"#/$defs/FieldCondition/properties/op/enum",keyword:"enum",params:{allowedValues: schema114.properties.op.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err90]; } @@ -6314,8 +7191,11 @@ vErrors.push(err90); errors++; } } -else { -const err91 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(data26.value !== undefined){ +let data29 = data26.value; +if(typeof data29 === "string"){ +if(func2(data29) > 100){ +const err91 = {instancePath:instancePath+"/where/" + i0+"/value",schemaPath:"#/$defs/FieldCondition/properties/value/maxLength",keyword:"maxLength",params:{limit: 100},message:"must NOT have more than 100 characters"}; if(vErrors === null){ vErrors = [err91]; } @@ -6324,11 +7204,8 @@ vErrors.push(err91); } errors++; } -} -if(data.kind !== undefined){ -let data26 = data.kind; -if(typeof data26 !== "string"){ -const err92 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/4/properties/kind/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(func2(data29) < 1){ +const err92 = {instancePath:instancePath+"/where/" + i0+"/value",schemaPath:"#/$defs/FieldCondition/properties/value/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}; if(vErrors === null){ vErrors = [err92]; } @@ -6337,8 +7214,9 @@ vErrors.push(err92); } errors++; } -if(!(data26 === "component")){ -const err93 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/4/properties/kind/enum",keyword:"enum",params:{allowedValues: schema92.oneOf[4].properties.kind.enum},message:"must be equal to one of the allowed values"}; +} +else { +const err93 = {instancePath:instancePath+"/where/" + i0+"/value",schemaPath:"#/$defs/FieldCondition/properties/value/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ vErrors = [err93]; } @@ -6348,11 +7226,11 @@ vErrors.push(err93); errors++; } } -if(data.type !== undefined){ -let data27 = data.type; -if(typeof data27 === "string"){ -if(!pattern51.test(data27)){ -const err94 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/4/properties/type/pattern",keyword:"pattern",params:{pattern: "^[a-z][a-z0-9-]{1,40}$"},message:"must match pattern \""+"^[a-z][a-z0-9-]{1,40}$"+"\""}; +if(data26.values !== undefined){ +let data30 = data26.values; +if(Array.isArray(data30)){ +if(data30.length > 20){ +const err94 = {instancePath:instancePath+"/where/" + i0+"/values",schemaPath:"#/$defs/FieldCondition/properties/values/maxItems",keyword:"maxItems",params:{limit: 20},message:"must NOT have more than 20 items"}; if(vErrors === null){ vErrors = [err94]; } @@ -6361,9 +7239,8 @@ vErrors.push(err94); } errors++; } -} -else { -const err95 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/4/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(data30.length < 1){ +const err95 = {instancePath:instancePath+"/where/" + i0+"/values",schemaPath:"#/$defs/FieldCondition/properties/values/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}; if(vErrors === null){ vErrors = [err95]; } @@ -6372,13 +7249,12 @@ vErrors.push(err95); } errors++; } -} -if(data.props !== undefined){ -let data28 = data.props; -if(data28 && typeof data28 == "object" && !Array.isArray(data28)){ -} -else { -const err96 = {instancePath:instancePath+"/props",schemaPath:"#/oneOf/4/properties/props/type",keyword:"type",params:{type: "object"},message:"must be object"}; +const len1 = data30.length; +for(let i1=0; i1 100){ +const err96 = {instancePath:instancePath+"/where/" + i0+"/values/" + i1,schemaPath:"#/$defs/FieldCondition/properties/values/items/maxLength",keyword:"maxLength",params:{limit: 100},message:"must NOT have more than 100 characters"}; if(vErrors === null){ vErrors = [err96]; } @@ -6387,10 +7263,8 @@ vErrors.push(err96); } errors++; } -} -} -else { -const err97 = {instancePath,schemaPath:"#/oneOf/4/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(func2(data31) < 1){ +const err97 = {instancePath:instancePath+"/where/" + i0+"/values/" + i1,schemaPath:"#/$defs/FieldCondition/properties/values/items/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}; if(vErrors === null){ vErrors = [err97]; } @@ -6399,23 +7273,9 @@ vErrors.push(err97); } errors++; } -var _valid0 = _errs68 === errors; -if(_valid0 && valid0){ -valid0 = false; -passing0 = [passing0, 4]; } else { -if(_valid0){ -valid0 = true; -passing0 = 4; -if(props0 !== true){ -props0 = true; -} -} -const _errs81 = errors; -if(data && typeof data == "object" && !Array.isArray(data)){ -if(data.id === undefined){ -const err98 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "id"},message:"must have required property '"+"id"+"'"}; +const err98 = {instancePath:instancePath+"/where/" + i0+"/values/" + i1,schemaPath:"#/$defs/FieldCondition/properties/values/items/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ vErrors = [err98]; } @@ -6424,8 +7284,19 @@ vErrors.push(err98); } errors++; } -if(data.kind === undefined){ -const err99 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "kind"},message:"must have required property '"+"kind"+"'"}; +} +let i2 = data30.length; +let j0; +if(i2 > 1){ +const indices0 = {}; +for(;i2--;){ +let item0 = data30[i2]; +if(typeof item0 !== "string"){ +continue; +} +if(typeof indices0[item0] == "number"){ +j0 = indices0[item0]; +const err99 = {instancePath:instancePath+"/where/" + i0+"/values",schemaPath:"#/$defs/FieldCondition/properties/values/uniqueItems",keyword:"uniqueItems",params:{i: i2, j: j0},message:"must NOT have duplicate items (items ## "+j0+" and "+i2+" are identical)"}; if(vErrors === null){ vErrors = [err99]; } @@ -6433,9 +7304,14 @@ else { vErrors.push(err99); } errors++; +break; } -if(data.catalog === undefined){ -const err100 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "catalog"},message:"must have required property '"+"catalog"+"'"}; +indices0[item0] = i2; +} +} +} +else { +const err100 = {instancePath:instancePath+"/where/" + i0+"/values",schemaPath:"#/$defs/FieldCondition/properties/values/type",keyword:"type",params:{type: "array"},message:"must be array"}; if(vErrors === null){ vErrors = [err100]; } @@ -6444,8 +7320,10 @@ vErrors.push(err100); } errors++; } -if(data.components === undefined){ -const err101 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "components"},message:"must have required property '"+"components"+"'"}; +} +} +else { +const err101 = {instancePath:instancePath+"/where/" + i0,schemaPath:"#/$defs/FieldCondition/type",keyword:"type",params:{type: "object"},message:"must be object"}; if(vErrors === null){ vErrors = [err101]; } @@ -6454,9 +7332,10 @@ vErrors.push(err101); } errors++; } -for(const key6 in data){ -if(!(((((key6 === "id") || (key6 === "kind")) || (key6 === "catalog")) || (key6 === "components")) || (key6 === "data"))){ -const err102 = {instancePath,schemaPath:"#/oneOf/5/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key6},message:"must NOT have additional properties"}; +} +} +else { +const err102 = {instancePath:instancePath+"/where",schemaPath:"#/oneOf/3/properties/where/type",keyword:"type",params:{type: "array"},message:"must be array"}; if(vErrors === null){ vErrors = [err102]; } @@ -6466,11 +7345,11 @@ vErrors.push(err102); errors++; } } -if(data.id !== undefined){ -let data29 = data.id; -if(typeof data29 === "string"){ -if(func2(data29) > 48){ -const err103 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/maxLength",keyword:"maxLength",params:{limit: 48},message:"must NOT have more than 48 characters"}; +if(data.orderBy !== undefined){ +let data32 = data.orderBy; +if(data32 && typeof data32 == "object" && !Array.isArray(data32)){ +if(data32.field === undefined){ +const err103 = {instancePath:instancePath+"/orderBy",schemaPath:"#/$defs/FieldOrder/required",keyword:"required",params:{missingProperty: "field"},message:"must have required property '"+"field"+"'"}; if(vErrors === null){ vErrors = [err103]; } @@ -6479,8 +7358,8 @@ vErrors.push(err103); } errors++; } -if(func2(data29) < 8){ -const err104 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/minLength",keyword:"minLength",params:{limit: 8},message:"must NOT have fewer than 8 characters"}; +if(data32.direction === undefined){ +const err104 = {instancePath:instancePath+"/orderBy",schemaPath:"#/$defs/FieldOrder/required",keyword:"required",params:{missingProperty: "direction"},message:"must have required property '"+"direction"+"'"}; if(vErrors === null){ vErrors = [err104]; } @@ -6489,8 +7368,9 @@ vErrors.push(err104); } errors++; } -if(!pattern4.test(data29)){ -const err105 = {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}$"+"\""}; +for(const key6 in data32){ +if(!((key6 === "field") || (key6 === "direction"))){ +const err105 = {instancePath:instancePath+"/orderBy",schemaPath:"#/$defs/FieldOrder/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key6},message:"must NOT have additional properties"}; if(vErrors === null){ vErrors = [err105]; } @@ -6500,8 +7380,11 @@ vErrors.push(err105); errors++; } } -else { -const err106 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(data32.field !== undefined){ +let data33 = data32.field; +if(typeof data33 === "string"){ +if(!pattern22.test(data33)){ +const err106 = {instancePath:instancePath+"/orderBy/field",schemaPath:"#/$defs/FieldOrder/properties/field/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 = [err106]; } @@ -6511,10 +7394,8 @@ vErrors.push(err106); errors++; } } -if(data.kind !== undefined){ -let data30 = data.kind; -if(typeof data30 !== "string"){ -const err107 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/5/properties/kind/type",keyword:"type",params:{type: "string"},message:"must be string"}; +else { +const err107 = {instancePath:instancePath+"/orderBy/field",schemaPath:"#/$defs/FieldOrder/properties/field/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ vErrors = [err107]; } @@ -6523,8 +7404,11 @@ vErrors.push(err107); } errors++; } -if(!(data30 === "surface")){ -const err108 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/5/properties/kind/enum",keyword:"enum",params:{allowedValues: schema92.oneOf[5].properties.kind.enum},message:"must be equal to one of the allowed values"}; +} +if(data32.direction !== undefined){ +let data34 = data32.direction; +if(typeof data34 !== "string"){ +const err108 = {instancePath:instancePath+"/orderBy/direction",schemaPath:"#/$defs/FieldOrder/properties/direction/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ vErrors = [err108]; } @@ -6533,12 +7417,8 @@ vErrors.push(err108); } errors++; } -} -if(data.catalog !== undefined){ -let data31 = data.catalog; -if(typeof data31 === "string"){ -if(!pattern53.test(data31)){ -const err109 = {instancePath:instancePath+"/catalog",schemaPath:"#/oneOf/5/properties/catalog/pattern",keyword:"pattern",params:{pattern: "^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"},message:"must match pattern \""+"^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"+"\""}; +if(!((data34 === "asc") || (data34 === "desc"))){ +const err109 = {instancePath:instancePath+"/orderBy/direction",schemaPath:"#/$defs/FieldOrder/properties/direction/enum",keyword:"enum",params:{allowedValues: schema115.properties.direction.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err109]; } @@ -6548,8 +7428,9 @@ vErrors.push(err109); errors++; } } +} else { -const err110 = {instancePath:instancePath+"/catalog",schemaPath:"#/oneOf/5/properties/catalog/type",keyword:"type",params:{type: "string"},message:"must be string"}; +const err110 = {instancePath:instancePath+"/orderBy",schemaPath:"#/$defs/FieldOrder/type",keyword:"type",params:{type: "object"},message:"must be object"}; if(vErrors === null){ vErrors = [err110]; } @@ -6559,11 +7440,9 @@ vErrors.push(err110); errors++; } } -if(data.components !== undefined){ -let data32 = data.components; -if(Array.isArray(data32)){ -if(data32.length > 200){ -const err111 = {instancePath:instancePath+"/components",schemaPath:"#/oneOf/5/properties/components/maxItems",keyword:"maxItems",params:{limit: 200},message:"must NOT have more than 200 items"}; +} +else { +const err111 = {instancePath,schemaPath:"#/oneOf/3/type",keyword:"type",params:{type: "object"},message:"must be object"}; if(vErrors === null){ vErrors = [err111]; } @@ -6572,8 +7451,23 @@ vErrors.push(err111); } errors++; } -if(data32.length < 1){ -const err112 = {instancePath:instancePath+"/components",schemaPath:"#/oneOf/5/properties/components/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}; +var _valid0 = _errs51 === errors; +if(_valid0 && valid0){ +valid0 = false; +passing0 = [passing0, 3]; +} +else { +if(_valid0){ +valid0 = true; +passing0 = 3; +if(props0 !== true){ +props0 = true; +} +} +const _errs92 = errors; +if(data && typeof data == "object" && !Array.isArray(data)){ +if(data.id === undefined){ +const err112 = {instancePath,schemaPath:"#/oneOf/4/required",keyword:"required",params:{missingProperty: "id"},message:"must have required property '"+"id"+"'"}; if(vErrors === null){ vErrors = [err112]; } @@ -6582,52 +7476,396 @@ vErrors.push(err112); } errors++; } -const len0 = data32.length; -for(let i0=0; i0 48){ +const err117 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/maxLength",keyword:"maxLength",params:{limit: 48},message:"must NOT have more than 48 characters"}; +if(vErrors === null){ +vErrors = [err117]; +} +else { +vErrors.push(err117); +} +errors++; +} +if(func2(data35) < 8){ +const err118 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/minLength",keyword:"minLength",params:{limit: 8},message:"must NOT have fewer than 8 characters"}; +if(vErrors === null){ +vErrors = [err118]; +} +else { +vErrors.push(err118); +} +errors++; +} +if(!pattern4.test(data35)){ +const err119 = {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 = [err119]; +} +else { +vErrors.push(err119); +} +errors++; +} +} +else { +const err120 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err120]; +} +else { +vErrors.push(err120); +} +errors++; +} +} +if(data.kind !== undefined){ +let data36 = data.kind; +if(typeof data36 !== "string"){ +const err121 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/4/properties/kind/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err121]; +} +else { +vErrors.push(err121); +} +errors++; +} +if(!(data36 === "component")){ +const err122 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/4/properties/kind/enum",keyword:"enum",params:{allowedValues: schema100.oneOf[4].properties.kind.enum},message:"must be equal to one of the allowed values"}; +if(vErrors === null){ +vErrors = [err122]; +} +else { +vErrors.push(err122); +} +errors++; +} +} +if(data.type !== undefined){ +let data37 = data.type; +if(typeof data37 === "string"){ +if(!pattern58.test(data37)){ +const err123 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/4/properties/type/pattern",keyword:"pattern",params:{pattern: "^[a-z][a-z0-9-]{1,40}$"},message:"must match pattern \""+"^[a-z][a-z0-9-]{1,40}$"+"\""}; +if(vErrors === null){ +vErrors = [err123]; +} +else { +vErrors.push(err123); +} +errors++; +} +} +else { +const err124 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/4/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err124]; +} +else { +vErrors.push(err124); +} +errors++; +} +} +if(data.props !== undefined){ +let data38 = data.props; +if(data38 && typeof data38 == "object" && !Array.isArray(data38)){ +} +else { +const err125 = {instancePath:instancePath+"/props",schemaPath:"#/oneOf/4/properties/props/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err125]; +} +else { +vErrors.push(err125); +} +errors++; +} +} +} +else { +const err126 = {instancePath,schemaPath:"#/oneOf/4/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err126]; +} +else { +vErrors.push(err126); +} +errors++; +} +var _valid0 = _errs92 === errors; +if(_valid0 && valid0){ +valid0 = false; +passing0 = [passing0, 4]; +} +else { +if(_valid0){ +valid0 = true; +passing0 = 4; +if(props0 !== true){ +props0 = true; +} +} +const _errs105 = errors; +if(data && typeof data == "object" && !Array.isArray(data)){ +if(data.id === undefined){ +const err127 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "id"},message:"must have required property '"+"id"+"'"}; +if(vErrors === null){ +vErrors = [err127]; +} +else { +vErrors.push(err127); +} +errors++; +} +if(data.kind === undefined){ +const err128 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "kind"},message:"must have required property '"+"kind"+"'"}; +if(vErrors === null){ +vErrors = [err128]; +} +else { +vErrors.push(err128); +} +errors++; +} +if(data.catalog === undefined){ +const err129 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "catalog"},message:"must have required property '"+"catalog"+"'"}; +if(vErrors === null){ +vErrors = [err129]; +} +else { +vErrors.push(err129); +} +errors++; +} +if(data.components === undefined){ +const err130 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "components"},message:"must have required property '"+"components"+"'"}; +if(vErrors === null){ +vErrors = [err130]; +} +else { +vErrors.push(err130); +} +errors++; +} +for(const key8 in data){ +if(!(((((key8 === "id") || (key8 === "kind")) || (key8 === "catalog")) || (key8 === "components")) || (key8 === "data"))){ +const err131 = {instancePath,schemaPath:"#/oneOf/5/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key8},message:"must NOT have additional properties"}; +if(vErrors === null){ +vErrors = [err131]; +} +else { +vErrors.push(err131); +} +errors++; +} +} +if(data.id !== undefined){ +let data39 = data.id; +if(typeof data39 === "string"){ +if(func2(data39) > 48){ +const err132 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/maxLength",keyword:"maxLength",params:{limit: 48},message:"must NOT have more than 48 characters"}; +if(vErrors === null){ +vErrors = [err132]; +} +else { +vErrors.push(err132); +} +errors++; +} +if(func2(data39) < 8){ +const err133 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/minLength",keyword:"minLength",params:{limit: 8},message:"must NOT have fewer than 8 characters"}; +if(vErrors === null){ +vErrors = [err133]; +} +else { +vErrors.push(err133); +} +errors++; +} +if(!pattern4.test(data39)){ +const err134 = {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 = [err134]; +} +else { +vErrors.push(err134); +} +errors++; +} +} +else { +const err135 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err135]; +} +else { +vErrors.push(err135); +} +errors++; +} +} +if(data.kind !== undefined){ +let data40 = data.kind; +if(typeof data40 !== "string"){ +const err136 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/5/properties/kind/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err136]; +} +else { +vErrors.push(err136); +} +errors++; +} +if(!(data40 === "surface")){ +const err137 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/5/properties/kind/enum",keyword:"enum",params:{allowedValues: schema100.oneOf[5].properties.kind.enum},message:"must be equal to one of the allowed values"}; +if(vErrors === null){ +vErrors = [err137]; +} +else { +vErrors.push(err137); +} +errors++; +} +} +if(data.catalog !== undefined){ +let data41 = data.catalog; +if(typeof data41 === "string"){ +if(!pattern60.test(data41)){ +const err138 = {instancePath:instancePath+"/catalog",schemaPath:"#/oneOf/5/properties/catalog/pattern",keyword:"pattern",params:{pattern: "^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"},message:"must match pattern \""+"^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"+"\""}; +if(vErrors === null){ +vErrors = [err138]; +} +else { +vErrors.push(err138); +} +errors++; +} +} +else { +const err139 = {instancePath:instancePath+"/catalog",schemaPath:"#/oneOf/5/properties/catalog/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err139]; +} +else { +vErrors.push(err139); +} +errors++; +} +} +if(data.components !== undefined){ +let data42 = data.components; +if(Array.isArray(data42)){ +if(data42.length > 200){ +const err140 = {instancePath:instancePath+"/components",schemaPath:"#/oneOf/5/properties/components/maxItems",keyword:"maxItems",params:{limit: 200},message:"must NOT have more than 200 items"}; +if(vErrors === null){ +vErrors = [err140]; +} +else { +vErrors.push(err140); +} +errors++; +} +if(data42.length < 1){ +const err141 = {instancePath:instancePath+"/components",schemaPath:"#/oneOf/5/properties/components/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}; +if(vErrors === null){ +vErrors = [err141]; +} +else { +vErrors.push(err141); +} +errors++; +} +const len2 = data42.length; +for(let i3=0; i3*'.","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}}}; +export const validatePage = validate85; +const schema130 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://cloudhal.dev/schemas/v1/page.schema.json","title":"Page","description":"A routable page of a site: a path, editorial status and an ordered list of sections. Sections are embedded (not separate nodes) so a page is the unit of locking, snapshotting and diffing.","type":"object","additionalProperties":false,"required":["schemaVersion","id","siteId","path","title","locale","status","sections","rev","createdAt","updatedAt"],"properties":{"schemaVersion":{"$ref":"#/$defs/SchemaVersion"},"id":{"$ref":"#/$defs/Id"},"siteId":{"$ref":"#/$defs/Id"},"path":{"type":"string","description":"Absolute URL path, starting with '/', no trailing slash except root.","pattern":"^/(?:[a-z0-9]+(?:-[a-z0-9]+)*(?:/[a-z0-9]+(?:-[a-z0-9]+)*)*)?$","maxLength":1024},"title":{"type":"string","minLength":1,"maxLength":300},"locale":{"$ref":"#/$defs/Locale"},"status":{"$ref":"#/$defs/PublishStatus"},"sections":{"type":"array","items":{"$ref":"#/$defs/Section"}},"seo":{"$ref":"#/$defs/Seo"},"availability":{"$ref":"#/$defs/Availability"},"chrome":{"type":"object","description":"This page's own header and footer (RFC-0025 §1): a landing page without the menu, a checkout without the footer.","additionalProperties":false,"properties":{"header":{"type":"string","enum":["default","transparent","hidden"]},"footer":{"type":"string","enum":["default","minimal","hidden"]}}},"motion":{"$ref":"#/$defs/MotionLevel","description":"Overrides the site's motion level for this page, up or down (RFC-0024 §1); absent follows the site."},"rev":{"$ref":"#/$defs/Revision"},"createdAt":{"$ref":"#/$defs/Timestamp"},"updatedAt":{"$ref":"#/$defs/Timestamp"},"translation":{"$ref":"#/$defs/Translation"},"metadata":{"$ref":"#/$defs/Metadata"}},"$defs":{"Actor":{"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}},"ActorKind":{"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]},"Availability":{"type":"object","description":"Time gates of a page or content entry (RFC-0008): the published site shows the node only from `from` (inclusive: an embargo) until `until` (exclusive: an expiry), checked at render time, so the gates hold even when no scheduler runs. Before `from` and from `until` on, the node is absent from every public route, list, feed, sitemap and search output; previews still show it. At least one gate; writers check that `from` is before `until`.","additionalProperties":false,"minProperties":1,"properties":{"from":{"$ref":"#/$defs/Timestamp"},"until":{"$ref":"#/$defs/Timestamp"}}},"BodyBlock":{"type":"object","description":"One top-level block of a markdown body in the store's index (RFC-0014 §3). Written by the store only: a client's values are replaced on every write. `by`, `via` and `at` say who last changed the block's content, through which channel and when; they are absent for content written before the index existed.","additionalProperties":false,"required":["id","hash"],"properties":{"id":{"type":"string","pattern":"^blk_[0-9A-Za-z]{8,24}$","description":"Stable across writes that keep the block's content or its place among unchanged blocks."},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"SHA-256 (hex) of the block's markdown without trailing whitespace."},"by":{"$ref":"#/$defs/Actor"},"via":{"type":"string","enum":["studio","mcp","api","import","ai-command","proposal"],"description":"The channel of that change."},"at":{"$ref":"#/$defs/Timestamp"}}},"BodyBlocks":{"type":"array","description":"A markdown body's block index, in body order (RFC-0014 §3). Absent means not indexed (yet, or an HTML body), never empty.","maxItems":5000,"items":{"$ref":"#/$defs/BodyBlock"}},"BodyProfile":{"type":"integer","enum":[1],"description":"The markdown dialect a body follows (RFC-0014 §1): 1 = Cloudhal Markdown v1 (CommonMark + GFM + `asset:` + directives; raw HTML escaped). Set by the store on markdown bodies."},"FieldCondition":{"type":"object","description":"RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`.","additionalProperties":false,"required":["field","op"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","in","set","unset"]},"value":{"type":"string","minLength":1,"maxLength":100},"values":{"type":"array","minItems":1,"maxItems":20,"uniqueItems":true,"items":{"type":"string","minLength":1,"maxLength":100}}}},"FieldOrder":{"type":"object","description":"RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last.","additionalProperties":false,"required":["field","direction"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"direction":{"type":"string","enum":["asc","desc"]}}},"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"ImageRef":{"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}},"Link":{"type":"object","additionalProperties":false,"required":["label","href"],"properties":{"label":{"type":"string","minLength":1,"maxLength":200},"href":{"type":"string","minLength":1,"maxLength":2048},"external":{"type":"boolean"}}},"Locale":{"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"},"Metadata":{"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}},"MotionLevel":{"type":"string","description":"How much a page may move (RFC-0024 §1); the reader's reduced-motion preference and pause control can only lower it.","enum":["none","subtle","standard","expressive"]},"PublishStatus":{"type":"string","description":"Editorial lifecycle of a page or content entry.","enum":["draft","review","published","archived"]},"Revision":{"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0},"SchemaVersion":{"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]},"Section":{"title":"Section","description":"A block inside a page, discriminated by `kind`. Variants are written inline (not via $ref) and `kind` is a single-value string enum so that TypeScript gets a literal union and Rust gets a serde internally-tagged enum.","oneOf":[{"type":"object","title":"HeroSection","additionalProperties":false,"required":["id","kind","heading"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["hero"]},"heading":{"type":"string","minLength":1,"maxLength":300},"subheading":{"type":"string","maxLength":1000},"image":{"$ref":"#/$defs/ImageRef"},"cta":{"$ref":"#/$defs/Link"}}},{"type":"object","title":"RichTextSection","additionalProperties":false,"required":["id","kind","format","body"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["richText"]},"format":{"type":"string","enum":["markdown","html"]},"body":{"type":"string","maxLength":200000},"profile":{"$ref":"#/$defs/BodyProfile"},"blocks":{"$ref":"#/$defs/BodyBlocks"}}},{"type":"object","title":"ImageSection","additionalProperties":false,"required":["id","kind","image"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["image"]},"image":{"$ref":"#/$defs/ImageRef"},"caption":{"type":"string","maxLength":1000}}},{"type":"object","title":"ContentListSection","description":"Renders the newest entries of a content collection (e.g. blog posts).","additionalProperties":false,"required":["id","kind","collection","limit","sort"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["contentList"]},"collection":{"$ref":"#/$defs/Slug"},"limit":{"type":"integer","minimum":1,"maximum":100},"sort":{"type":"string","enum":["newest","oldest","title"]},"filter":{"type":"string","description":"Filter expression over term assignments (RFC-0002), e.g. 'category:news+tag:ai' or 'category:news>*'.","minLength":1,"maxLength":512},"where":{"type":"array","description":"RFC-0042: conditions on the collection's list fields, ANDed.","minItems":1,"maxItems":4,"items":{"$ref":"#/$defs/FieldCondition"}},"orderBy":{"$ref":"#/$defs/FieldOrder"}}},{"type":"object","title":"ComponentSection","description":"A typed block from the block registry (RFC-0015): `type` names a registered block type (one global namespace of English names), `props` follow that type's props schema, which the store validates.","additionalProperties":false,"required":["id","kind","type","props"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["component"]},"type":{"type":"string","pattern":"^[a-z][a-z0-9-]{1,40}$"},"props":{"type":"object","description":"Validated against the type's props schema by the store, not by this contract.","additionalProperties":true}}},{"type":"object","title":"SurfaceSection","description":"A generative UI surface (RFC-0020): a flat list of catalog components with a `root`, plus a data model their props may bind to. Written and streamed by agents as data, never code; the store checks the tree, the catalog and the limits.","additionalProperties":false,"required":["id","kind","catalog","components"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["surface"]},"catalog":{"type":"string","description":"The catalog the surface was written for: `@` (RFC-0018 manifest).","pattern":"^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"},"components":{"type":"array","minItems":1,"maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponent"}},"data":{"type":"object","description":"The surface's data model: JSON its components bind to with `{path}` (at most 64 KB serialized; the store enforces the size).","additionalProperties":true}}}]},"Seo":{"type":"object","description":"Search and social metadata of a routable node (page, term archive).","additionalProperties":false,"properties":{"title":{"type":"string","maxLength":300},"description":{"type":"string","maxLength":500},"noindex":{"type":"boolean"},"canonical":{"type":"string","maxLength":2048},"ogImage":{"$ref":"#/$defs/ImageRef"}}},"Slug":{"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128},"SurfaceComponent":{"type":"object","description":"One element of a generative UI surface (RFC-0020 §1), in a flat list: `component` names a catalog entry (a PascalCase built-in primitive such as `Stack`, or a registry block type such as `callout`), `children` refer to other ids of the same surface, and `props` follow the entry's schema, which the store checks. Field names follow A2UI v0.9.","additionalProperties":false,"required":["id","component"],"properties":{"id":{"$ref":"#/$defs/SurfaceComponentId"},"component":{"type":"string","pattern":"^(?:[A-Z][A-Za-z0-9]{1,40}|[a-z][a-z0-9-]{1,40})$"},"children":{"type":"array","maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponentId"}},"props":{"type":"object","description":"Literal values, `{path}` bindings into the surface's data, or `{call}` named functions; validated by the store against the catalog, not by this contract.","additionalProperties":true},"visible":{"type":"object","description":"A visibility condition over the surface's data (`eq`, `ne`, `gt`, `lt`, `and`, `or`, `not`); validated by the store.","additionalProperties":true},"motion":{"type":"object","description":"Motion presets for this component (RFC-0024 §2): `reveal`, `hover`, `speed`, `delay`, `stagger`, `replay` and later `scroll`, `loop`; enums and clamped numbers only, validated by the store against the catalog. The renderer turns them into CSS; agents never write CSS.","additionalProperties":true}}},"SurfaceComponentId":{"type":"string","description":"A component's id, unique within its surface; `root` is the tree's root (RFC-0020 §1).","pattern":"^[a-z][a-z0-9-]{0,40}$"},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"},"Translation":{"type":"object","description":"Links a page or entry to the other language versions of the same work (RFC-0033 §2). Staleness is computed: the source's current hash differs from source.hash.","additionalProperties":false,"required":["group","method"],"properties":{"group":{"$ref":"#/$defs/Id","description":"The group's id: the original node's id, shared by every translation. May cross sites in one project."},"method":{"$ref":"#/$defs/TranslationMethod"},"layout":{"type":"string","description":"linked: the original's structural changes propagate to this translation as drafts (RFC-0033 §4a); independent: nothing propagates. Absent means linked.","enum":["linked","independent"]},"source":{"type":"object","description":"The node and revision this translation was made from. Absent on the original.","additionalProperties":false,"required":["id","locale","rev","hash"],"properties":{"id":{"$ref":"#/$defs/Id"},"siteId":{"$ref":"#/$defs/Id","description":"The source's site when it is another site of the same project (a language edition). Absent means this site."},"locale":{"$ref":"#/$defs/Locale"},"rev":{"$ref":"#/$defs/Revision"},"hash":{"type":"string","description":"Lower-case hex SHA-256 of the source node's canonical JSON at that revision.","pattern":"^[0-9a-f]{64}$"}}},"provenance":{"type":"object","description":"The machine-translation record: engine, glossary revision, checks run, quality score. Required when method is machine or machine-reviewed.","additionalProperties":false,"required":["engine","at"],"properties":{"engine":{"type":"string","minLength":1,"maxLength":200},"glossaryRev":{"$ref":"#/$defs/Revision"},"score":{"type":"number","minimum":0,"maximum":100},"checks":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":50},"at":{"$ref":"#/$defs/Timestamp"}}},"review":{"type":"object","description":"The person who reviewed this translation and when. Only humans set it.","additionalProperties":false,"required":["by","at"],"properties":{"by":{"$ref":"#/$defs/Actor"},"at":{"$ref":"#/$defs/Timestamp"}}}}},"TranslationMethod":{"type":"string","description":"How a page's or entry's text came to be (RFC-0033). 'original' marks the source of a translation group; 'machine-reviewed' is machine output a person has reviewed.","enum":["original","human","machine","machine-reviewed"]}}}; +const schema131 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; +const schema132 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; +const schema134 = {"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"}; +const schema135 = {"type":"string","description":"Editorial lifecycle of a page or content entry.","enum":["draft","review","published","archived"]}; +const schema163 = {"type":"string","description":"How much a page may move (RFC-0024 §1); the reader's reduced-motion preference and pause control can only lower it.","enum":["none","subtle","standard","expressive"]}; +const schema164 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; +const schema148 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema177 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; +const schema136 = {"title":"Section","description":"A block inside a page, discriminated by `kind`. Variants are written inline (not via $ref) and `kind` is a single-value string enum so that TypeScript gets a literal union and Rust gets a serde internally-tagged enum.","oneOf":[{"type":"object","title":"HeroSection","additionalProperties":false,"required":["id","kind","heading"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["hero"]},"heading":{"type":"string","minLength":1,"maxLength":300},"subheading":{"type":"string","maxLength":1000},"image":{"$ref":"#/$defs/ImageRef"},"cta":{"$ref":"#/$defs/Link"}}},{"type":"object","title":"RichTextSection","additionalProperties":false,"required":["id","kind","format","body"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["richText"]},"format":{"type":"string","enum":["markdown","html"]},"body":{"type":"string","maxLength":200000},"profile":{"$ref":"#/$defs/BodyProfile"},"blocks":{"$ref":"#/$defs/BodyBlocks"}}},{"type":"object","title":"ImageSection","additionalProperties":false,"required":["id","kind","image"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["image"]},"image":{"$ref":"#/$defs/ImageRef"},"caption":{"type":"string","maxLength":1000}}},{"type":"object","title":"ContentListSection","description":"Renders the newest entries of a content collection (e.g. blog posts).","additionalProperties":false,"required":["id","kind","collection","limit","sort"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["contentList"]},"collection":{"$ref":"#/$defs/Slug"},"limit":{"type":"integer","minimum":1,"maximum":100},"sort":{"type":"string","enum":["newest","oldest","title"]},"filter":{"type":"string","description":"Filter expression over term assignments (RFC-0002), e.g. 'category:news+tag:ai' or 'category:news>*'.","minLength":1,"maxLength":512},"where":{"type":"array","description":"RFC-0042: conditions on the collection's list fields, ANDed.","minItems":1,"maxItems":4,"items":{"$ref":"#/$defs/FieldCondition"}},"orderBy":{"$ref":"#/$defs/FieldOrder"}}},{"type":"object","title":"ComponentSection","description":"A typed block from the block registry (RFC-0015): `type` names a registered block type (one global namespace of English names), `props` follow that type's props schema, which the store validates.","additionalProperties":false,"required":["id","kind","type","props"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["component"]},"type":{"type":"string","pattern":"^[a-z][a-z0-9-]{1,40}$"},"props":{"type":"object","description":"Validated against the type's props schema by the store, not by this contract.","additionalProperties":true}}},{"type":"object","title":"SurfaceSection","description":"A generative UI surface (RFC-0020): a flat list of catalog components with a `root`, plus a data model their props may bind to. Written and streamed by agents as data, never code; the store checks the tree, the catalog and the limits.","additionalProperties":false,"required":["id","kind","catalog","components"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["surface"]},"catalog":{"type":"string","description":"The catalog the surface was written for: `@` (RFC-0018 manifest).","pattern":"^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"},"components":{"type":"array","minItems":1,"maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponent"}},"data":{"type":"object","description":"The surface's data model: JSON its components bind to with `{path}` (at most 64 KB serialized; the store enforces the size).","additionalProperties":true}}}]}; +const schema140 = {"type":"object","additionalProperties":false,"required":["label","href"],"properties":{"label":{"type":"string","minLength":1,"maxLength":200},"href":{"type":"string","minLength":1,"maxLength":2048},"external":{"type":"boolean"}}}; +const schema142 = {"type":"integer","enum":[1],"description":"The markdown dialect a body follows (RFC-0014 §1): 1 = Cloudhal Markdown v1 (CommonMark + GFM + `asset:` + directives; raw HTML escaped). Set by the store on markdown bodies."}; +const schema151 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; +const schema152 = {"type":"object","description":"RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`.","additionalProperties":false,"required":["field","op"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","in","set","unset"]},"value":{"type":"string","minLength":1,"maxLength":100},"values":{"type":"array","minItems":1,"maxItems":20,"uniqueItems":true,"items":{"type":"string","minLength":1,"maxLength":100}}}}; +const schema153 = {"type":"object","description":"RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last.","additionalProperties":false,"required":["field","direction"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"direction":{"type":"string","enum":["asc","desc"]}}}; +const schema138 = {"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}}; -function validate81(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate87(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate81.evaluated; +const evaluated0 = validate87.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -8781,20 +10021,20 @@ vErrors.push(err16); } errors++; } -validate81.errors = vErrors; +validate87.errors = vErrors; return errors === 0; } -validate81.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate87.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -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"]}; +const schema143 = {"type":"array","description":"A markdown body's block index, in body order (RFC-0014 §3). Absent means not indexed (yet, or an HTML body), never empty.","maxItems":5000,"items":{"$ref":"#/$defs/BodyBlock"}}; +const schema144 = {"type":"object","description":"One top-level block of a markdown body in the store's index (RFC-0014 §3). Written by the store only: a client's values are replaced on every write. `by`, `via` and `at` say who last changed the block's content, through which channel and when; they are absent for content written before the index existed.","additionalProperties":false,"required":["id","hash"],"properties":{"id":{"type":"string","pattern":"^blk_[0-9A-Za-z]{8,24}$","description":"Stable across writes that keep the block's content or its place among unchanged blocks."},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"SHA-256 (hex) of the block's markdown without trailing whitespace."},"by":{"$ref":"#/$defs/Actor"},"via":{"type":"string","enum":["studio","mcp","api","import","ai-command","proposal"],"description":"The channel of that change."},"at":{"$ref":"#/$defs/Timestamp"}}}; +const schema145 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; +const schema146 = {"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]}; -function validate85(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate91(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate85.evaluated; +const evaluated0 = validate91.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -8847,7 +10087,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: schema136.enum},message:"must be equal to one of the allowed values"}; +const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema146.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -8938,16 +10178,16 @@ vErrors.push(err11); } errors++; } -validate85.errors = vErrors; +validate91.errors = vErrors; return errors === 0; } -validate85.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate91.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate84(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate90(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate84.evaluated; +const evaluated0 = validate90.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -8990,7 +10230,7 @@ errors++; if(data.id !== undefined){ let data0 = data.id; if(typeof data0 === "string"){ -if(!pattern44.test(data0)){ +if(!pattern49.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]; @@ -9015,7 +10255,7 @@ errors++; if(data.hash !== undefined){ let data1 = data.hash; if(typeof data1 === "string"){ -if(!pattern45.test(data1)){ +if(!pattern50.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]; @@ -9038,8 +10278,8 @@ errors++; } } if(data.by !== undefined){ -if(!(validate85(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate85.errors : vErrors.concat(validate85.errors); +if(!(validate91(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate91.errors : vErrors.concat(validate91.errors); errors = vErrors.length; } } @@ -9056,7 +10296,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: schema134.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: schema144.properties.via.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err8]; } @@ -9102,16 +10342,16 @@ vErrors.push(err11); } errors++; } -validate84.errors = vErrors; +validate90.errors = vErrors; return errors === 0; } -validate84.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate90.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate83(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate89(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate83.evaluated; +const evaluated0 = validate89.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -9131,8 +10371,8 @@ errors++; } const len0 = data.length; for(let i0=0; i0 4){ +const err82 = {instancePath:instancePath+"/where",schemaPath:"#/oneOf/3/properties/where/maxItems",keyword:"maxItems",params:{limit: 4},message:"must NOT have more than 4 items"}; if(vErrors === null){ vErrors = [err82]; } @@ -10369,23 +11611,8 @@ vErrors.push(err82); } errors++; } -var _valid0 = _errs51 === errors; -if(_valid0 && valid0){ -valid0 = false; -passing0 = [passing0, 3]; -} -else { -if(_valid0){ -valid0 = true; -passing0 = 3; -if(props0 !== true){ -props0 = true; -} -} -const _errs68 = errors; -if(data && typeof data == "object" && !Array.isArray(data)){ -if(data.id === undefined){ -const err83 = {instancePath,schemaPath:"#/oneOf/4/required",keyword:"required",params:{missingProperty: "id"},message:"must have required property '"+"id"+"'"}; +if(data25.length < 1){ +const err83 = {instancePath:instancePath+"/where",schemaPath:"#/oneOf/3/properties/where/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}; if(vErrors === null){ vErrors = [err83]; } @@ -10394,8 +11621,12 @@ vErrors.push(err83); } errors++; } -if(data.kind === undefined){ -const err84 = {instancePath,schemaPath:"#/oneOf/4/required",keyword:"required",params:{missingProperty: "kind"},message:"must have required property '"+"kind"+"'"}; +const len0 = data25.length; +for(let i0=0; i0 48){ -const err88 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/maxLength",keyword:"maxLength",params:{limit: 48},message:"must NOT have more than 48 characters"}; +else { +const err88 = {instancePath:instancePath+"/where/" + i0+"/field",schemaPath:"#/$defs/FieldCondition/properties/field/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ vErrors = [err88]; } @@ -10449,8 +11681,11 @@ vErrors.push(err88); } errors++; } -if(func2(data25) < 8){ -const err89 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/minLength",keyword:"minLength",params:{limit: 8},message:"must NOT have fewer than 8 characters"}; +} +if(data26.op !== undefined){ +let data28 = data26.op; +if(typeof data28 !== "string"){ +const err89 = {instancePath:instancePath+"/where/" + i0+"/op",schemaPath:"#/$defs/FieldCondition/properties/op/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ vErrors = [err89]; } @@ -10459,8 +11694,8 @@ vErrors.push(err89); } errors++; } -if(!pattern4.test(data25)){ -const err90 = {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(!(((((((((data28 === "eq") || (data28 === "ne")) || (data28 === "lt")) || (data28 === "lte")) || (data28 === "gt")) || (data28 === "gte")) || (data28 === "in")) || (data28 === "set")) || (data28 === "unset"))){ +const err90 = {instancePath:instancePath+"/where/" + i0+"/op",schemaPath:"#/$defs/FieldCondition/properties/op/enum",keyword:"enum",params:{allowedValues: schema152.properties.op.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err90]; } @@ -10470,8 +11705,11 @@ vErrors.push(err90); errors++; } } -else { -const err91 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(data26.value !== undefined){ +let data29 = data26.value; +if(typeof data29 === "string"){ +if(func2(data29) > 100){ +const err91 = {instancePath:instancePath+"/where/" + i0+"/value",schemaPath:"#/$defs/FieldCondition/properties/value/maxLength",keyword:"maxLength",params:{limit: 100},message:"must NOT have more than 100 characters"}; if(vErrors === null){ vErrors = [err91]; } @@ -10480,11 +11718,8 @@ vErrors.push(err91); } errors++; } -} -if(data.kind !== undefined){ -let data26 = data.kind; -if(typeof data26 !== "string"){ -const err92 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/4/properties/kind/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(func2(data29) < 1){ +const err92 = {instancePath:instancePath+"/where/" + i0+"/value",schemaPath:"#/$defs/FieldCondition/properties/value/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}; if(vErrors === null){ vErrors = [err92]; } @@ -10493,8 +11728,9 @@ vErrors.push(err92); } errors++; } -if(!(data26 === "component")){ -const err93 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/4/properties/kind/enum",keyword:"enum",params:{allowedValues: schema126.oneOf[4].properties.kind.enum},message:"must be equal to one of the allowed values"}; +} +else { +const err93 = {instancePath:instancePath+"/where/" + i0+"/value",schemaPath:"#/$defs/FieldCondition/properties/value/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ vErrors = [err93]; } @@ -10504,11 +11740,11 @@ vErrors.push(err93); errors++; } } -if(data.type !== undefined){ -let data27 = data.type; -if(typeof data27 === "string"){ -if(!pattern51.test(data27)){ -const err94 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/4/properties/type/pattern",keyword:"pattern",params:{pattern: "^[a-z][a-z0-9-]{1,40}$"},message:"must match pattern \""+"^[a-z][a-z0-9-]{1,40}$"+"\""}; +if(data26.values !== undefined){ +let data30 = data26.values; +if(Array.isArray(data30)){ +if(data30.length > 20){ +const err94 = {instancePath:instancePath+"/where/" + i0+"/values",schemaPath:"#/$defs/FieldCondition/properties/values/maxItems",keyword:"maxItems",params:{limit: 20},message:"must NOT have more than 20 items"}; if(vErrors === null){ vErrors = [err94]; } @@ -10517,9 +11753,8 @@ vErrors.push(err94); } errors++; } -} -else { -const err95 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/4/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(data30.length < 1){ +const err95 = {instancePath:instancePath+"/where/" + i0+"/values",schemaPath:"#/$defs/FieldCondition/properties/values/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}; if(vErrors === null){ vErrors = [err95]; } @@ -10528,13 +11763,12 @@ vErrors.push(err95); } errors++; } -} -if(data.props !== undefined){ -let data28 = data.props; -if(data28 && typeof data28 == "object" && !Array.isArray(data28)){ -} -else { -const err96 = {instancePath:instancePath+"/props",schemaPath:"#/oneOf/4/properties/props/type",keyword:"type",params:{type: "object"},message:"must be object"}; +const len1 = data30.length; +for(let i1=0; i1 100){ +const err96 = {instancePath:instancePath+"/where/" + i0+"/values/" + i1,schemaPath:"#/$defs/FieldCondition/properties/values/items/maxLength",keyword:"maxLength",params:{limit: 100},message:"must NOT have more than 100 characters"}; if(vErrors === null){ vErrors = [err96]; } @@ -10543,10 +11777,8 @@ vErrors.push(err96); } errors++; } -} -} -else { -const err97 = {instancePath,schemaPath:"#/oneOf/4/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(func2(data31) < 1){ +const err97 = {instancePath:instancePath+"/where/" + i0+"/values/" + i1,schemaPath:"#/$defs/FieldCondition/properties/values/items/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}; if(vErrors === null){ vErrors = [err97]; } @@ -10555,23 +11787,9 @@ vErrors.push(err97); } errors++; } -var _valid0 = _errs68 === errors; -if(_valid0 && valid0){ -valid0 = false; -passing0 = [passing0, 4]; } else { -if(_valid0){ -valid0 = true; -passing0 = 4; -if(props0 !== true){ -props0 = true; -} -} -const _errs81 = errors; -if(data && typeof data == "object" && !Array.isArray(data)){ -if(data.id === undefined){ -const err98 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "id"},message:"must have required property '"+"id"+"'"}; +const err98 = {instancePath:instancePath+"/where/" + i0+"/values/" + i1,schemaPath:"#/$defs/FieldCondition/properties/values/items/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ vErrors = [err98]; } @@ -10580,8 +11798,19 @@ vErrors.push(err98); } errors++; } -if(data.kind === undefined){ -const err99 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "kind"},message:"must have required property '"+"kind"+"'"}; +} +let i2 = data30.length; +let j0; +if(i2 > 1){ +const indices0 = {}; +for(;i2--;){ +let item0 = data30[i2]; +if(typeof item0 !== "string"){ +continue; +} +if(typeof indices0[item0] == "number"){ +j0 = indices0[item0]; +const err99 = {instancePath:instancePath+"/where/" + i0+"/values",schemaPath:"#/$defs/FieldCondition/properties/values/uniqueItems",keyword:"uniqueItems",params:{i: i2, j: j0},message:"must NOT have duplicate items (items ## "+j0+" and "+i2+" are identical)"}; if(vErrors === null){ vErrors = [err99]; } @@ -10589,9 +11818,14 @@ else { vErrors.push(err99); } errors++; +break; } -if(data.catalog === undefined){ -const err100 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "catalog"},message:"must have required property '"+"catalog"+"'"}; +indices0[item0] = i2; +} +} +} +else { +const err100 = {instancePath:instancePath+"/where/" + i0+"/values",schemaPath:"#/$defs/FieldCondition/properties/values/type",keyword:"type",params:{type: "array"},message:"must be array"}; if(vErrors === null){ vErrors = [err100]; } @@ -10600,8 +11834,10 @@ vErrors.push(err100); } errors++; } -if(data.components === undefined){ -const err101 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "components"},message:"must have required property '"+"components"+"'"}; +} +} +else { +const err101 = {instancePath:instancePath+"/where/" + i0,schemaPath:"#/$defs/FieldCondition/type",keyword:"type",params:{type: "object"},message:"must be object"}; if(vErrors === null){ vErrors = [err101]; } @@ -10610,9 +11846,10 @@ vErrors.push(err101); } errors++; } -for(const key6 in data){ -if(!(((((key6 === "id") || (key6 === "kind")) || (key6 === "catalog")) || (key6 === "components")) || (key6 === "data"))){ -const err102 = {instancePath,schemaPath:"#/oneOf/5/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key6},message:"must NOT have additional properties"}; +} +} +else { +const err102 = {instancePath:instancePath+"/where",schemaPath:"#/oneOf/3/properties/where/type",keyword:"type",params:{type: "array"},message:"must be array"}; if(vErrors === null){ vErrors = [err102]; } @@ -10622,11 +11859,11 @@ vErrors.push(err102); errors++; } } -if(data.id !== undefined){ -let data29 = data.id; -if(typeof data29 === "string"){ -if(func2(data29) > 48){ -const err103 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/maxLength",keyword:"maxLength",params:{limit: 48},message:"must NOT have more than 48 characters"}; +if(data.orderBy !== undefined){ +let data32 = data.orderBy; +if(data32 && typeof data32 == "object" && !Array.isArray(data32)){ +if(data32.field === undefined){ +const err103 = {instancePath:instancePath+"/orderBy",schemaPath:"#/$defs/FieldOrder/required",keyword:"required",params:{missingProperty: "field"},message:"must have required property '"+"field"+"'"}; if(vErrors === null){ vErrors = [err103]; } @@ -10635,8 +11872,8 @@ vErrors.push(err103); } errors++; } -if(func2(data29) < 8){ -const err104 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/minLength",keyword:"minLength",params:{limit: 8},message:"must NOT have fewer than 8 characters"}; +if(data32.direction === undefined){ +const err104 = {instancePath:instancePath+"/orderBy",schemaPath:"#/$defs/FieldOrder/required",keyword:"required",params:{missingProperty: "direction"},message:"must have required property '"+"direction"+"'"}; if(vErrors === null){ vErrors = [err104]; } @@ -10645,8 +11882,9 @@ vErrors.push(err104); } errors++; } -if(!pattern4.test(data29)){ -const err105 = {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}$"+"\""}; +for(const key6 in data32){ +if(!((key6 === "field") || (key6 === "direction"))){ +const err105 = {instancePath:instancePath+"/orderBy",schemaPath:"#/$defs/FieldOrder/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key6},message:"must NOT have additional properties"}; if(vErrors === null){ vErrors = [err105]; } @@ -10656,8 +11894,11 @@ vErrors.push(err105); errors++; } } -else { -const err106 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(data32.field !== undefined){ +let data33 = data32.field; +if(typeof data33 === "string"){ +if(!pattern22.test(data33)){ +const err106 = {instancePath:instancePath+"/orderBy/field",schemaPath:"#/$defs/FieldOrder/properties/field/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 = [err106]; } @@ -10667,10 +11908,8 @@ vErrors.push(err106); errors++; } } -if(data.kind !== undefined){ -let data30 = data.kind; -if(typeof data30 !== "string"){ -const err107 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/5/properties/kind/type",keyword:"type",params:{type: "string"},message:"must be string"}; +else { +const err107 = {instancePath:instancePath+"/orderBy/field",schemaPath:"#/$defs/FieldOrder/properties/field/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ vErrors = [err107]; } @@ -10679,8 +11918,11 @@ vErrors.push(err107); } errors++; } -if(!(data30 === "surface")){ -const err108 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/5/properties/kind/enum",keyword:"enum",params:{allowedValues: schema126.oneOf[5].properties.kind.enum},message:"must be equal to one of the allowed values"}; +} +if(data32.direction !== undefined){ +let data34 = data32.direction; +if(typeof data34 !== "string"){ +const err108 = {instancePath:instancePath+"/orderBy/direction",schemaPath:"#/$defs/FieldOrder/properties/direction/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ vErrors = [err108]; } @@ -10689,12 +11931,8 @@ vErrors.push(err108); } errors++; } -} -if(data.catalog !== undefined){ -let data31 = data.catalog; -if(typeof data31 === "string"){ -if(!pattern53.test(data31)){ -const err109 = {instancePath:instancePath+"/catalog",schemaPath:"#/oneOf/5/properties/catalog/pattern",keyword:"pattern",params:{pattern: "^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"},message:"must match pattern \""+"^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"+"\""}; +if(!((data34 === "asc") || (data34 === "desc"))){ +const err109 = {instancePath:instancePath+"/orderBy/direction",schemaPath:"#/$defs/FieldOrder/properties/direction/enum",keyword:"enum",params:{allowedValues: schema153.properties.direction.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err109]; } @@ -10704,8 +11942,9 @@ vErrors.push(err109); errors++; } } +} else { -const err110 = {instancePath:instancePath+"/catalog",schemaPath:"#/oneOf/5/properties/catalog/type",keyword:"type",params:{type: "string"},message:"must be string"}; +const err110 = {instancePath:instancePath+"/orderBy",schemaPath:"#/$defs/FieldOrder/type",keyword:"type",params:{type: "object"},message:"must be object"}; if(vErrors === null){ vErrors = [err110]; } @@ -10715,11 +11954,9 @@ vErrors.push(err110); errors++; } } -if(data.components !== undefined){ -let data32 = data.components; -if(Array.isArray(data32)){ -if(data32.length > 200){ -const err111 = {instancePath:instancePath+"/components",schemaPath:"#/oneOf/5/properties/components/maxItems",keyword:"maxItems",params:{limit: 200},message:"must NOT have more than 200 items"}; +} +else { +const err111 = {instancePath,schemaPath:"#/oneOf/3/type",keyword:"type",params:{type: "object"},message:"must be object"}; if(vErrors === null){ vErrors = [err111]; } @@ -10728,8 +11965,23 @@ vErrors.push(err111); } errors++; } -if(data32.length < 1){ -const err112 = {instancePath:instancePath+"/components",schemaPath:"#/oneOf/5/properties/components/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}; +var _valid0 = _errs51 === errors; +if(_valid0 && valid0){ +valid0 = false; +passing0 = [passing0, 3]; +} +else { +if(_valid0){ +valid0 = true; +passing0 = 3; +if(props0 !== true){ +props0 = true; +} +} +const _errs92 = errors; +if(data && typeof data == "object" && !Array.isArray(data)){ +if(data.id === undefined){ +const err112 = {instancePath,schemaPath:"#/oneOf/4/required",keyword:"required",params:{missingProperty: "id"},message:"must have required property '"+"id"+"'"}; if(vErrors === null){ vErrors = [err112]; } @@ -10738,52 +11990,396 @@ vErrors.push(err112); } errors++; } -const len0 = data32.length; -for(let i0=0; i0 48){ +const err117 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/maxLength",keyword:"maxLength",params:{limit: 48},message:"must NOT have more than 48 characters"}; +if(vErrors === null){ +vErrors = [err117]; +} +else { +vErrors.push(err117); +} +errors++; +} +if(func2(data35) < 8){ +const err118 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/minLength",keyword:"minLength",params:{limit: 8},message:"must NOT have fewer than 8 characters"}; +if(vErrors === null){ +vErrors = [err118]; +} +else { +vErrors.push(err118); +} +errors++; +} +if(!pattern4.test(data35)){ +const err119 = {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 = [err119]; +} +else { +vErrors.push(err119); +} +errors++; +} +} +else { +const err120 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err120]; +} +else { +vErrors.push(err120); +} +errors++; +} +} +if(data.kind !== undefined){ +let data36 = data.kind; +if(typeof data36 !== "string"){ +const err121 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/4/properties/kind/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err121]; +} +else { +vErrors.push(err121); +} +errors++; +} +if(!(data36 === "component")){ +const err122 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/4/properties/kind/enum",keyword:"enum",params:{allowedValues: schema136.oneOf[4].properties.kind.enum},message:"must be equal to one of the allowed values"}; +if(vErrors === null){ +vErrors = [err122]; +} +else { +vErrors.push(err122); +} +errors++; +} +} +if(data.type !== undefined){ +let data37 = data.type; +if(typeof data37 === "string"){ +if(!pattern58.test(data37)){ +const err123 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/4/properties/type/pattern",keyword:"pattern",params:{pattern: "^[a-z][a-z0-9-]{1,40}$"},message:"must match pattern \""+"^[a-z][a-z0-9-]{1,40}$"+"\""}; +if(vErrors === null){ +vErrors = [err123]; +} +else { +vErrors.push(err123); +} +errors++; +} +} +else { +const err124 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/4/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err124]; +} +else { +vErrors.push(err124); +} +errors++; +} +} +if(data.props !== undefined){ +let data38 = data.props; +if(data38 && typeof data38 == "object" && !Array.isArray(data38)){ +} +else { +const err125 = {instancePath:instancePath+"/props",schemaPath:"#/oneOf/4/properties/props/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err125]; +} +else { +vErrors.push(err125); +} +errors++; +} +} +} +else { +const err126 = {instancePath,schemaPath:"#/oneOf/4/type",keyword:"type",params:{type: "object"},message:"must be object"}; +if(vErrors === null){ +vErrors = [err126]; +} +else { +vErrors.push(err126); +} +errors++; +} +var _valid0 = _errs92 === errors; +if(_valid0 && valid0){ +valid0 = false; +passing0 = [passing0, 4]; +} +else { +if(_valid0){ +valid0 = true; +passing0 = 4; +if(props0 !== true){ +props0 = true; +} +} +const _errs105 = errors; +if(data && typeof data == "object" && !Array.isArray(data)){ +if(data.id === undefined){ +const err127 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "id"},message:"must have required property '"+"id"+"'"}; +if(vErrors === null){ +vErrors = [err127]; +} +else { +vErrors.push(err127); +} +errors++; +} +if(data.kind === undefined){ +const err128 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "kind"},message:"must have required property '"+"kind"+"'"}; +if(vErrors === null){ +vErrors = [err128]; +} +else { +vErrors.push(err128); +} +errors++; +} +if(data.catalog === undefined){ +const err129 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "catalog"},message:"must have required property '"+"catalog"+"'"}; +if(vErrors === null){ +vErrors = [err129]; +} +else { +vErrors.push(err129); +} +errors++; +} +if(data.components === undefined){ +const err130 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "components"},message:"must have required property '"+"components"+"'"}; +if(vErrors === null){ +vErrors = [err130]; +} +else { +vErrors.push(err130); +} +errors++; +} +for(const key8 in data){ +if(!(((((key8 === "id") || (key8 === "kind")) || (key8 === "catalog")) || (key8 === "components")) || (key8 === "data"))){ +const err131 = {instancePath,schemaPath:"#/oneOf/5/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key8},message:"must NOT have additional properties"}; +if(vErrors === null){ +vErrors = [err131]; +} +else { +vErrors.push(err131); +} +errors++; +} +} +if(data.id !== undefined){ +let data39 = data.id; +if(typeof data39 === "string"){ +if(func2(data39) > 48){ +const err132 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/maxLength",keyword:"maxLength",params:{limit: 48},message:"must NOT have more than 48 characters"}; +if(vErrors === null){ +vErrors = [err132]; +} +else { +vErrors.push(err132); +} +errors++; +} +if(func2(data39) < 8){ +const err133 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/minLength",keyword:"minLength",params:{limit: 8},message:"must NOT have fewer than 8 characters"}; +if(vErrors === null){ +vErrors = [err133]; +} +else { +vErrors.push(err133); +} +errors++; +} +if(!pattern4.test(data39)){ +const err134 = {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 = [err134]; +} +else { +vErrors.push(err134); +} +errors++; +} +} +else { +const err135 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err135]; +} +else { +vErrors.push(err135); +} +errors++; +} +} +if(data.kind !== undefined){ +let data40 = data.kind; +if(typeof data40 !== "string"){ +const err136 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/5/properties/kind/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err136]; +} +else { +vErrors.push(err136); +} +errors++; +} +if(!(data40 === "surface")){ +const err137 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/5/properties/kind/enum",keyword:"enum",params:{allowedValues: schema136.oneOf[5].properties.kind.enum},message:"must be equal to one of the allowed values"}; +if(vErrors === null){ +vErrors = [err137]; +} +else { +vErrors.push(err137); +} +errors++; +} +} +if(data.catalog !== undefined){ +let data41 = data.catalog; +if(typeof data41 === "string"){ +if(!pattern60.test(data41)){ +const err138 = {instancePath:instancePath+"/catalog",schemaPath:"#/oneOf/5/properties/catalog/pattern",keyword:"pattern",params:{pattern: "^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"},message:"must match pattern \""+"^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"+"\""}; +if(vErrors === null){ +vErrors = [err138]; +} +else { +vErrors.push(err138); +} +errors++; +} +} +else { +const err139 = {instancePath:instancePath+"/catalog",schemaPath:"#/oneOf/5/properties/catalog/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err139]; +} +else { +vErrors.push(err139); +} +errors++; +} +} +if(data.components !== undefined){ +let data42 = data.components; +if(Array.isArray(data42)){ +if(data42.length > 200){ +const err140 = {instancePath:instancePath+"/components",schemaPath:"#/oneOf/5/properties/components/maxItems",keyword:"maxItems",params:{limit: 200},message:"must NOT have more than 200 items"}; +if(vErrors === null){ +vErrors = [err140]; +} +else { +vErrors.push(err140); +} +errors++; +} +if(data42.length < 1){ +const err141 = {instancePath:instancePath+"/components",schemaPath:"#/oneOf/5/properties/components/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}; +if(vErrors === null){ +vErrors = [err141]; +} +else { +vErrors.push(err141); +} +errors++; +} +const len2 = data42.length; +for(let i3=0; i3*'.","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}}}; +export const validateSection = validate107; +const schema178 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://cloudhal.dev/schemas/v1/section.schema.json","title":"Section","description":"A block inside a page, discriminated by `kind`. Variants are written inline (not via $ref) and `kind` is a single-value string enum so that TypeScript gets a literal union and Rust gets a serde internally-tagged enum.","oneOf":[{"type":"object","title":"HeroSection","additionalProperties":false,"required":["id","kind","heading"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["hero"]},"heading":{"type":"string","minLength":1,"maxLength":300},"subheading":{"type":"string","maxLength":1000},"image":{"$ref":"#/$defs/ImageRef"},"cta":{"$ref":"#/$defs/Link"}}},{"type":"object","title":"RichTextSection","additionalProperties":false,"required":["id","kind","format","body"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["richText"]},"format":{"type":"string","enum":["markdown","html"]},"body":{"type":"string","maxLength":200000},"profile":{"$ref":"#/$defs/BodyProfile"},"blocks":{"$ref":"#/$defs/BodyBlocks"}}},{"type":"object","title":"ImageSection","additionalProperties":false,"required":["id","kind","image"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["image"]},"image":{"$ref":"#/$defs/ImageRef"},"caption":{"type":"string","maxLength":1000}}},{"type":"object","title":"ContentListSection","description":"Renders the newest entries of a content collection (e.g. blog posts).","additionalProperties":false,"required":["id","kind","collection","limit","sort"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["contentList"]},"collection":{"$ref":"#/$defs/Slug"},"limit":{"type":"integer","minimum":1,"maximum":100},"sort":{"type":"string","enum":["newest","oldest","title"]},"filter":{"type":"string","description":"Filter expression over term assignments (RFC-0002), e.g. 'category:news+tag:ai' or 'category:news>*'.","minLength":1,"maxLength":512},"where":{"type":"array","description":"RFC-0042: conditions on the collection's list fields, ANDed.","minItems":1,"maxItems":4,"items":{"$ref":"#/$defs/FieldCondition"}},"orderBy":{"$ref":"#/$defs/FieldOrder"}}},{"type":"object","title":"ComponentSection","description":"A typed block from the block registry (RFC-0015): `type` names a registered block type (one global namespace of English names), `props` follow that type's props schema, which the store validates.","additionalProperties":false,"required":["id","kind","type","props"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["component"]},"type":{"type":"string","pattern":"^[a-z][a-z0-9-]{1,40}$"},"props":{"type":"object","description":"Validated against the type's props schema by the store, not by this contract.","additionalProperties":true}}},{"type":"object","title":"SurfaceSection","description":"A generative UI surface (RFC-0020): a flat list of catalog components with a `root`, plus a data model their props may bind to. Written and streamed by agents as data, never code; the store checks the tree, the catalog and the limits.","additionalProperties":false,"required":["id","kind","catalog","components"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["surface"]},"catalog":{"type":"string","description":"The catalog the surface was written for: `@` (RFC-0018 manifest).","pattern":"^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"},"components":{"type":"array","minItems":1,"maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponent"}},"data":{"type":"object","description":"The surface's data model: JSON its components bind to with `{path}` (at most 64 KB serialized; the store enforces the size).","additionalProperties":true}}}],"$defs":{"Actor":{"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}},"ActorKind":{"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]},"BodyBlock":{"type":"object","description":"One top-level block of a markdown body in the store's index (RFC-0014 §3). Written by the store only: a client's values are replaced on every write. `by`, `via` and `at` say who last changed the block's content, through which channel and when; they are absent for content written before the index existed.","additionalProperties":false,"required":["id","hash"],"properties":{"id":{"type":"string","pattern":"^blk_[0-9A-Za-z]{8,24}$","description":"Stable across writes that keep the block's content or its place among unchanged blocks."},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"SHA-256 (hex) of the block's markdown without trailing whitespace."},"by":{"$ref":"#/$defs/Actor"},"via":{"type":"string","enum":["studio","mcp","api","import","ai-command","proposal"],"description":"The channel of that change."},"at":{"$ref":"#/$defs/Timestamp"}}},"BodyBlocks":{"type":"array","description":"A markdown body's block index, in body order (RFC-0014 §3). Absent means not indexed (yet, or an HTML body), never empty.","maxItems":5000,"items":{"$ref":"#/$defs/BodyBlock"}},"BodyProfile":{"type":"integer","enum":[1],"description":"The markdown dialect a body follows (RFC-0014 §1): 1 = Cloudhal Markdown v1 (CommonMark + GFM + `asset:` + directives; raw HTML escaped). Set by the store on markdown bodies."},"FieldCondition":{"type":"object","description":"RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`.","additionalProperties":false,"required":["field","op"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","in","set","unset"]},"value":{"type":"string","minLength":1,"maxLength":100},"values":{"type":"array","minItems":1,"maxItems":20,"uniqueItems":true,"items":{"type":"string","minLength":1,"maxLength":100}}}},"FieldOrder":{"type":"object","description":"RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last.","additionalProperties":false,"required":["field","direction"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"direction":{"type":"string","enum":["asc","desc"]}}},"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"ImageRef":{"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}},"Link":{"type":"object","additionalProperties":false,"required":["label","href"],"properties":{"label":{"type":"string","minLength":1,"maxLength":200},"href":{"type":"string","minLength":1,"maxLength":2048},"external":{"type":"boolean"}}},"Slug":{"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128},"SurfaceComponent":{"type":"object","description":"One element of a generative UI surface (RFC-0020 §1), in a flat list: `component` names a catalog entry (a PascalCase built-in primitive such as `Stack`, or a registry block type such as `callout`), `children` refer to other ids of the same surface, and `props` follow the entry's schema, which the store checks. Field names follow A2UI v0.9.","additionalProperties":false,"required":["id","component"],"properties":{"id":{"$ref":"#/$defs/SurfaceComponentId"},"component":{"type":"string","pattern":"^(?:[A-Z][A-Za-z0-9]{1,40}|[a-z][a-z0-9-]{1,40})$"},"children":{"type":"array","maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponentId"}},"props":{"type":"object","description":"Literal values, `{path}` bindings into the surface's data, or `{call}` named functions; validated by the store against the catalog, not by this contract.","additionalProperties":true},"visible":{"type":"object","description":"A visibility condition over the surface's data (`eq`, `ne`, `gt`, `lt`, `and`, `or`, `not`); validated by the store.","additionalProperties":true},"motion":{"type":"object","description":"Motion presets for this component (RFC-0024 §2): `reveal`, `hover`, `speed`, `delay`, `stagger`, `replay` and later `scroll`, `loop`; enums and clamped numbers only, validated by the store against the catalog. The renderer turns them into CSS; agents never write CSS.","additionalProperties":true}}},"SurfaceComponentId":{"type":"string","description":"A component's id, unique within its surface; `root` is the tree's root (RFC-0020 §1).","pattern":"^[a-z][a-z0-9-]{0,40}$"},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}}}; +const schema179 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; +const schema182 = {"type":"object","additionalProperties":false,"required":["label","href"],"properties":{"label":{"type":"string","minLength":1,"maxLength":200},"href":{"type":"string","minLength":1,"maxLength":2048},"external":{"type":"boolean"}}}; +const schema184 = {"type":"integer","enum":[1],"description":"The markdown dialect a body follows (RFC-0014 §1): 1 = Cloudhal Markdown v1 (CommonMark + GFM + `asset:` + directives; raw HTML escaped). Set by the store on markdown bodies."}; +const schema193 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; +const schema194 = {"type":"object","description":"RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`.","additionalProperties":false,"required":["field","op"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","in","set","unset"]},"value":{"type":"string","minLength":1,"maxLength":100},"values":{"type":"array","minItems":1,"maxItems":20,"uniqueItems":true,"items":{"type":"string","minLength":1,"maxLength":100}}}}; +const schema195 = {"type":"object","description":"RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last.","additionalProperties":false,"required":["field","direction"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"direction":{"type":"string","enum":["asc","desc"]}}}; +const schema180 = {"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}}; -function validate102(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate108(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate102.evaluated; +const evaluated0 = validate108.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -12591,21 +14189,21 @@ vErrors.push(err16); } errors++; } -validate102.errors = vErrors; +validate108.errors = vErrors; return errors === 0; } -validate102.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate108.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -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"]}; +const schema185 = {"type":"array","description":"A markdown body's block index, in body order (RFC-0014 §3). Absent means not indexed (yet, or an HTML body), never empty.","maxItems":5000,"items":{"$ref":"#/$defs/BodyBlock"}}; +const schema186 = {"type":"object","description":"One top-level block of a markdown body in the store's index (RFC-0014 §3). Written by the store only: a client's values are replaced on every write. `by`, `via` and `at` say who last changed the block's content, through which channel and when; they are absent for content written before the index existed.","additionalProperties":false,"required":["id","hash"],"properties":{"id":{"type":"string","pattern":"^blk_[0-9A-Za-z]{8,24}$","description":"Stable across writes that keep the block's content or its place among unchanged blocks."},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"SHA-256 (hex) of the block's markdown without trailing whitespace."},"by":{"$ref":"#/$defs/Actor"},"via":{"type":"string","enum":["studio","mcp","api","import","ai-command","proposal"],"description":"The channel of that change."},"at":{"$ref":"#/$defs/Timestamp"}}}; +const schema190 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema187 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; +const schema188 = {"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]}; -function validate106(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate112(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate106.evaluated; +const evaluated0 = validate112.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -12658,7 +14256,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: schema176.enum},message:"must be equal to one of the allowed values"}; +const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema188.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -12749,16 +14347,16 @@ vErrors.push(err11); } errors++; } -validate106.errors = vErrors; +validate112.errors = vErrors; return errors === 0; } -validate106.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate112.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate105(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate111(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate105.evaluated; +const evaluated0 = validate111.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -12801,7 +14399,7 @@ errors++; if(data.id !== undefined){ let data0 = data.id; if(typeof data0 === "string"){ -if(!pattern44.test(data0)){ +if(!pattern49.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]; @@ -12826,7 +14424,7 @@ errors++; if(data.hash !== undefined){ let data1 = data.hash; if(typeof data1 === "string"){ -if(!pattern45.test(data1)){ +if(!pattern50.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]; @@ -12849,8 +14447,8 @@ errors++; } } if(data.by !== undefined){ -if(!(validate106(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate106.errors : vErrors.concat(validate106.errors); +if(!(validate112(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate112.errors : vErrors.concat(validate112.errors); errors = vErrors.length; } } @@ -12867,7 +14465,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: schema174.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: schema186.properties.via.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err8]; } @@ -12913,16 +14511,16 @@ vErrors.push(err11); } errors++; } -validate105.errors = vErrors; +validate111.errors = vErrors; return errors === 0; } -validate105.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate111.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate104(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate110(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate104.evaluated; +const evaluated0 = validate110.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -12942,8 +14540,8 @@ errors++; } const len0 = data.length; for(let i0=0; i0 128){ +const err70 = {instancePath:instancePath+"/collection",schemaPath:"#/$defs/Slug/maxLength",keyword:"maxLength",params:{limit: 128},message:"must NOT have more than 128 characters"}; +if(vErrors === null){ +vErrors = [err70]; +} +else { +vErrors.push(err70); +} +errors++; +} +if(func2(data21) < 1){ +const err71 = {instancePath:instancePath+"/collection",schemaPath:"#/$defs/Slug/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}; +if(vErrors === null){ +vErrors = [err71]; +} +else { +vErrors.push(err71); +} +errors++; +} +if(!pattern5.test(data21)){ +const err72 = {instancePath:instancePath+"/collection",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 = [err72]; +} +else { +vErrors.push(err72); +} +errors++; +} +} +else { +const err73 = {instancePath:instancePath+"/collection",schemaPath:"#/$defs/Slug/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err73]; +} +else { +vErrors.push(err73); +} +errors++; +} +} +if(data.limit !== undefined){ +let data22 = data.limit; +if(!(((typeof data22 == "number") && (!(data22 % 1) && !isNaN(data22))) && (isFinite(data22)))){ +const err74 = {instancePath:instancePath+"/limit",schemaPath:"#/oneOf/3/properties/limit/type",keyword:"type",params:{type: "integer"},message:"must be integer"}; +if(vErrors === null){ +vErrors = [err74]; +} +else { +vErrors.push(err74); +} +errors++; +} +if((typeof data22 == "number") && (isFinite(data22))){ +if(data22 > 100 || isNaN(data22)){ +const err75 = {instancePath:instancePath+"/limit",schemaPath:"#/oneOf/3/properties/limit/maximum",keyword:"maximum",params:{comparison: "<=", limit: 100},message:"must be <= 100"}; +if(vErrors === null){ +vErrors = [err75]; +} +else { +vErrors.push(err75); +} +errors++; +} +if(data22 < 1 || isNaN(data22)){ +const err76 = {instancePath:instancePath+"/limit",schemaPath:"#/oneOf/3/properties/limit/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"}; +if(vErrors === null){ +vErrors = [err76]; +} +else { +vErrors.push(err76); +} +errors++; +} +} +} +if(data.sort !== undefined){ +let data23 = data.sort; +if(typeof data23 !== "string"){ +const err77 = {instancePath:instancePath+"/sort",schemaPath:"#/oneOf/3/properties/sort/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err77]; +} +else { +vErrors.push(err77); +} +errors++; +} +if(!(((data23 === "newest") || (data23 === "oldest")) || (data23 === "title"))){ +const err78 = {instancePath:instancePath+"/sort",schemaPath:"#/oneOf/3/properties/sort/enum",keyword:"enum",params:{allowedValues: schema178.oneOf[3].properties.sort.enum},message:"must be equal to one of the allowed values"}; +if(vErrors === null){ +vErrors = [err78]; +} +else { +vErrors.push(err78); +} +errors++; +} +} +if(data.filter !== undefined){ +let data24 = data.filter; +if(typeof data24 === "string"){ +if(func2(data24) > 512){ +const err79 = {instancePath:instancePath+"/filter",schemaPath:"#/oneOf/3/properties/filter/maxLength",keyword:"maxLength",params:{limit: 512},message:"must NOT have more than 512 characters"}; +if(vErrors === null){ +vErrors = [err79]; +} +else { +vErrors.push(err79); +} +errors++; +} +if(func2(data24) < 1){ +const err80 = {instancePath:instancePath+"/filter",schemaPath:"#/oneOf/3/properties/filter/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}; +if(vErrors === null){ +vErrors = [err80]; +} +else { +vErrors.push(err80); +} +errors++; +} +} +else { +const err81 = {instancePath:instancePath+"/filter",schemaPath:"#/oneOf/3/properties/filter/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err81]; +} +else { +vErrors.push(err81); +} +errors++; +} +} +if(data.where !== undefined){ +let data25 = data.where; +if(Array.isArray(data25)){ +if(data25.length > 4){ +const err82 = {instancePath:instancePath+"/where",schemaPath:"#/oneOf/3/properties/where/maxItems",keyword:"maxItems",params:{limit: 4},message:"must NOT have more than 4 items"}; +if(vErrors === null){ +vErrors = [err82]; +} +else { +vErrors.push(err82); +} +errors++; +} +if(data25.length < 1){ +const err83 = {instancePath:instancePath+"/where",schemaPath:"#/oneOf/3/properties/where/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}; +if(vErrors === null){ +vErrors = [err83]; +} +else { +vErrors.push(err83); +} +errors++; +} +const len0 = data25.length; +for(let i0=0; i0 100){ +const err91 = {instancePath:instancePath+"/where/" + i0+"/value",schemaPath:"#/$defs/FieldCondition/properties/value/maxLength",keyword:"maxLength",params:{limit: 100},message:"must NOT have more than 100 characters"}; +if(vErrors === null){ +vErrors = [err91]; +} +else { +vErrors.push(err91); +} +errors++; +} +if(func2(data29) < 1){ +const err92 = {instancePath:instancePath+"/where/" + i0+"/value",schemaPath:"#/$defs/FieldCondition/properties/value/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}; +if(vErrors === null){ +vErrors = [err92]; +} +else { +vErrors.push(err92); +} +errors++; +} +} +else { +const err93 = {instancePath:instancePath+"/where/" + i0+"/value",schemaPath:"#/$defs/FieldCondition/properties/value/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err93]; +} +else { +vErrors.push(err93); +} +errors++; +} +} +if(data26.values !== undefined){ +let data30 = data26.values; +if(Array.isArray(data30)){ +if(data30.length > 20){ +const err94 = {instancePath:instancePath+"/where/" + i0+"/values",schemaPath:"#/$defs/FieldCondition/properties/values/maxItems",keyword:"maxItems",params:{limit: 20},message:"must NOT have more than 20 items"}; +if(vErrors === null){ +vErrors = [err94]; +} +else { +vErrors.push(err94); +} +errors++; +} +if(data30.length < 1){ +const err95 = {instancePath:instancePath+"/where/" + i0+"/values",schemaPath:"#/$defs/FieldCondition/properties/values/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}; +if(vErrors === null){ +vErrors = [err95]; +} +else { +vErrors.push(err95); +} +errors++; +} +const len1 = data30.length; +for(let i1=0; i1 100){ +const err96 = {instancePath:instancePath+"/where/" + i0+"/values/" + i1,schemaPath:"#/$defs/FieldCondition/properties/values/items/maxLength",keyword:"maxLength",params:{limit: 100},message:"must NOT have more than 100 characters"}; +if(vErrors === null){ +vErrors = [err96]; } else { -vErrors.push(err65); +vErrors.push(err96); } errors++; } -if(!pattern4.test(data19)){ -const err66 = {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(func2(data31) < 1){ +const err97 = {instancePath:instancePath+"/where/" + i0+"/values/" + i1,schemaPath:"#/$defs/FieldCondition/properties/values/items/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}; if(vErrors === null){ -vErrors = [err66]; +vErrors = [err97]; } else { -vErrors.push(err66); +vErrors.push(err97); } errors++; } } else { -const err67 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/type",keyword:"type",params:{type: "string"},message:"must be string"}; +const err98 = {instancePath:instancePath+"/where/" + i0+"/values/" + i1,schemaPath:"#/$defs/FieldCondition/properties/values/items/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ -vErrors = [err67]; +vErrors = [err98]; } else { -vErrors.push(err67); +vErrors.push(err98); } errors++; } } -if(data.kind !== undefined){ -let data20 = data.kind; -if(typeof data20 !== "string"){ -const err68 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/3/properties/kind/type",keyword:"type",params:{type: "string"},message:"must be string"}; +let i2 = data30.length; +let j0; +if(i2 > 1){ +const indices0 = {}; +for(;i2--;){ +let item0 = data30[i2]; +if(typeof item0 !== "string"){ +continue; +} +if(typeof indices0[item0] == "number"){ +j0 = indices0[item0]; +const err99 = {instancePath:instancePath+"/where/" + i0+"/values",schemaPath:"#/$defs/FieldCondition/properties/values/uniqueItems",keyword:"uniqueItems",params:{i: i2, j: j0},message:"must NOT have duplicate items (items ## "+j0+" and "+i2+" are identical)"}; if(vErrors === null){ -vErrors = [err68]; +vErrors = [err99]; } else { -vErrors.push(err68); +vErrors.push(err99); } errors++; +break; } -if(!(data20 === "contentList")){ -const err69 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/3/properties/kind/enum",keyword:"enum",params:{allowedValues: schema166.oneOf[3].properties.kind.enum},message:"must be equal to one of the allowed values"}; -if(vErrors === null){ -vErrors = [err69]; -} -else { -vErrors.push(err69); +indices0[item0] = i2; } -errors++; } } -if(data.collection !== undefined){ -let data21 = data.collection; -if(typeof data21 === "string"){ -if(func2(data21) > 128){ -const err70 = {instancePath:instancePath+"/collection",schemaPath:"#/$defs/Slug/maxLength",keyword:"maxLength",params:{limit: 128},message:"must NOT have more than 128 characters"}; +else { +const err100 = {instancePath:instancePath+"/where/" + i0+"/values",schemaPath:"#/$defs/FieldCondition/properties/values/type",keyword:"type",params:{type: "array"},message:"must be array"}; if(vErrors === null){ -vErrors = [err70]; +vErrors = [err100]; } else { -vErrors.push(err70); +vErrors.push(err100); } errors++; } -if(func2(data21) < 1){ -const err71 = {instancePath:instancePath+"/collection",schemaPath:"#/$defs/Slug/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}; -if(vErrors === null){ -vErrors = [err71]; -} -else { -vErrors.push(err71); } -errors++; } -if(!pattern5.test(data21)){ -const err72 = {instancePath:instancePath+"/collection",schemaPath:"#/$defs/Slug/pattern",keyword:"pattern",params:{pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"},message:"must match pattern \""+"^[a-z0-9]+(?:-[a-z0-9]+)*$"+"\""}; +else { +const err101 = {instancePath:instancePath+"/where/" + i0,schemaPath:"#/$defs/FieldCondition/type",keyword:"type",params:{type: "object"},message:"must be object"}; if(vErrors === null){ -vErrors = [err72]; +vErrors = [err101]; } else { -vErrors.push(err72); +vErrors.push(err101); } errors++; } } +} else { -const err73 = {instancePath:instancePath+"/collection",schemaPath:"#/$defs/Slug/type",keyword:"type",params:{type: "string"},message:"must be string"}; +const err102 = {instancePath:instancePath+"/where",schemaPath:"#/oneOf/3/properties/where/type",keyword:"type",params:{type: "array"},message:"must be array"}; if(vErrors === null){ -vErrors = [err73]; +vErrors = [err102]; } else { -vErrors.push(err73); +vErrors.push(err102); } errors++; } } -if(data.limit !== undefined){ -let data22 = data.limit; -if(!(((typeof data22 == "number") && (!(data22 % 1) && !isNaN(data22))) && (isFinite(data22)))){ -const err74 = {instancePath:instancePath+"/limit",schemaPath:"#/oneOf/3/properties/limit/type",keyword:"type",params:{type: "integer"},message:"must be integer"}; +if(data.orderBy !== undefined){ +let data32 = data.orderBy; +if(data32 && typeof data32 == "object" && !Array.isArray(data32)){ +if(data32.field === undefined){ +const err103 = {instancePath:instancePath+"/orderBy",schemaPath:"#/$defs/FieldOrder/required",keyword:"required",params:{missingProperty: "field"},message:"must have required property '"+"field"+"'"}; if(vErrors === null){ -vErrors = [err74]; +vErrors = [err103]; } else { -vErrors.push(err74); +vErrors.push(err103); } errors++; } -if((typeof data22 == "number") && (isFinite(data22))){ -if(data22 > 100 || isNaN(data22)){ -const err75 = {instancePath:instancePath+"/limit",schemaPath:"#/oneOf/3/properties/limit/maximum",keyword:"maximum",params:{comparison: "<=", limit: 100},message:"must be <= 100"}; +if(data32.direction === undefined){ +const err104 = {instancePath:instancePath+"/orderBy",schemaPath:"#/$defs/FieldOrder/required",keyword:"required",params:{missingProperty: "direction"},message:"must have required property '"+"direction"+"'"}; if(vErrors === null){ -vErrors = [err75]; +vErrors = [err104]; } else { -vErrors.push(err75); +vErrors.push(err104); } errors++; } -if(data22 < 1 || isNaN(data22)){ -const err76 = {instancePath:instancePath+"/limit",schemaPath:"#/oneOf/3/properties/limit/minimum",keyword:"minimum",params:{comparison: ">=", limit: 1},message:"must be >= 1"}; +for(const key6 in data32){ +if(!((key6 === "field") || (key6 === "direction"))){ +const err105 = {instancePath:instancePath+"/orderBy",schemaPath:"#/$defs/FieldOrder/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key6},message:"must NOT have additional properties"}; if(vErrors === null){ -vErrors = [err76]; +vErrors = [err105]; } else { -vErrors.push(err76); +vErrors.push(err105); } errors++; } } -} -if(data.sort !== undefined){ -let data23 = data.sort; -if(typeof data23 !== "string"){ -const err77 = {instancePath:instancePath+"/sort",schemaPath:"#/oneOf/3/properties/sort/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(data32.field !== undefined){ +let data33 = data32.field; +if(typeof data33 === "string"){ +if(!pattern22.test(data33)){ +const err106 = {instancePath:instancePath+"/orderBy/field",schemaPath:"#/$defs/FieldOrder/properties/field/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 = [err77]; +vErrors = [err106]; } else { -vErrors.push(err77); +vErrors.push(err106); } errors++; } -if(!(((data23 === "newest") || (data23 === "oldest")) || (data23 === "title"))){ -const err78 = {instancePath:instancePath+"/sort",schemaPath:"#/oneOf/3/properties/sort/enum",keyword:"enum",params:{allowedValues: schema166.oneOf[3].properties.sort.enum},message:"must be equal to one of the allowed values"}; +} +else { +const err107 = {instancePath:instancePath+"/orderBy/field",schemaPath:"#/$defs/FieldOrder/properties/field/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ -vErrors = [err78]; +vErrors = [err107]; } else { -vErrors.push(err78); +vErrors.push(err107); } errors++; } } -if(data.filter !== undefined){ -let data24 = data.filter; -if(typeof data24 === "string"){ -if(func2(data24) > 512){ -const err79 = {instancePath:instancePath+"/filter",schemaPath:"#/oneOf/3/properties/filter/maxLength",keyword:"maxLength",params:{limit: 512},message:"must NOT have more than 512 characters"}; +if(data32.direction !== undefined){ +let data34 = data32.direction; +if(typeof data34 !== "string"){ +const err108 = {instancePath:instancePath+"/orderBy/direction",schemaPath:"#/$defs/FieldOrder/properties/direction/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ -vErrors = [err79]; +vErrors = [err108]; } else { -vErrors.push(err79); +vErrors.push(err108); } errors++; } -if(func2(data24) < 1){ -const err80 = {instancePath:instancePath+"/filter",schemaPath:"#/oneOf/3/properties/filter/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"}; +if(!((data34 === "asc") || (data34 === "desc"))){ +const err109 = {instancePath:instancePath+"/orderBy/direction",schemaPath:"#/$defs/FieldOrder/properties/direction/enum",keyword:"enum",params:{allowedValues: schema195.properties.direction.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ -vErrors = [err80]; +vErrors = [err109]; } else { -vErrors.push(err80); +vErrors.push(err109); } errors++; } } +} else { -const err81 = {instancePath:instancePath+"/filter",schemaPath:"#/oneOf/3/properties/filter/type",keyword:"type",params:{type: "string"},message:"must be string"}; +const err110 = {instancePath:instancePath+"/orderBy",schemaPath:"#/$defs/FieldOrder/type",keyword:"type",params:{type: "object"},message:"must be object"}; if(vErrors === null){ -vErrors = [err81]; +vErrors = [err110]; } else { -vErrors.push(err81); +vErrors.push(err110); } errors++; } } } else { -const err82 = {instancePath,schemaPath:"#/oneOf/3/type",keyword:"type",params:{type: "object"},message:"must be object"}; +const err111 = {instancePath,schemaPath:"#/oneOf/3/type",keyword:"type",params:{type: "object"},message:"must be object"}; if(vErrors === null){ -vErrors = [err82]; +vErrors = [err111]; } else { -vErrors.push(err82); +vErrors.push(err111); } errors++; } @@ -14194,180 +16148,180 @@ if(props0 !== true){ props0 = true; } } -const _errs68 = errors; +const _errs92 = errors; if(data && typeof data == "object" && !Array.isArray(data)){ if(data.id === undefined){ -const err83 = {instancePath,schemaPath:"#/oneOf/4/required",keyword:"required",params:{missingProperty: "id"},message:"must have required property '"+"id"+"'"}; +const err112 = {instancePath,schemaPath:"#/oneOf/4/required",keyword:"required",params:{missingProperty: "id"},message:"must have required property '"+"id"+"'"}; if(vErrors === null){ -vErrors = [err83]; +vErrors = [err112]; } else { -vErrors.push(err83); +vErrors.push(err112); } errors++; } if(data.kind === undefined){ -const err84 = {instancePath,schemaPath:"#/oneOf/4/required",keyword:"required",params:{missingProperty: "kind"},message:"must have required property '"+"kind"+"'"}; +const err113 = {instancePath,schemaPath:"#/oneOf/4/required",keyword:"required",params:{missingProperty: "kind"},message:"must have required property '"+"kind"+"'"}; if(vErrors === null){ -vErrors = [err84]; +vErrors = [err113]; } else { -vErrors.push(err84); +vErrors.push(err113); } errors++; } if(data.type === undefined){ -const err85 = {instancePath,schemaPath:"#/oneOf/4/required",keyword:"required",params:{missingProperty: "type"},message:"must have required property '"+"type"+"'"}; +const err114 = {instancePath,schemaPath:"#/oneOf/4/required",keyword:"required",params:{missingProperty: "type"},message:"must have required property '"+"type"+"'"}; if(vErrors === null){ -vErrors = [err85]; +vErrors = [err114]; } else { -vErrors.push(err85); +vErrors.push(err114); } errors++; } if(data.props === undefined){ -const err86 = {instancePath,schemaPath:"#/oneOf/4/required",keyword:"required",params:{missingProperty: "props"},message:"must have required property '"+"props"+"'"}; +const err115 = {instancePath,schemaPath:"#/oneOf/4/required",keyword:"required",params:{missingProperty: "props"},message:"must have required property '"+"props"+"'"}; if(vErrors === null){ -vErrors = [err86]; +vErrors = [err115]; } else { -vErrors.push(err86); +vErrors.push(err115); } errors++; } -for(const key5 in data){ -if(!((((key5 === "id") || (key5 === "kind")) || (key5 === "type")) || (key5 === "props"))){ -const err87 = {instancePath,schemaPath:"#/oneOf/4/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key5},message:"must NOT have additional properties"}; +for(const key7 in data){ +if(!((((key7 === "id") || (key7 === "kind")) || (key7 === "type")) || (key7 === "props"))){ +const err116 = {instancePath,schemaPath:"#/oneOf/4/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key7},message:"must NOT have additional properties"}; if(vErrors === null){ -vErrors = [err87]; +vErrors = [err116]; } else { -vErrors.push(err87); +vErrors.push(err116); } errors++; } } if(data.id !== undefined){ -let data25 = data.id; -if(typeof data25 === "string"){ -if(func2(data25) > 48){ -const err88 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/maxLength",keyword:"maxLength",params:{limit: 48},message:"must NOT have more than 48 characters"}; +let data35 = data.id; +if(typeof data35 === "string"){ +if(func2(data35) > 48){ +const err117 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/maxLength",keyword:"maxLength",params:{limit: 48},message:"must NOT have more than 48 characters"}; if(vErrors === null){ -vErrors = [err88]; +vErrors = [err117]; } else { -vErrors.push(err88); +vErrors.push(err117); } errors++; } -if(func2(data25) < 8){ -const err89 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/minLength",keyword:"minLength",params:{limit: 8},message:"must NOT have fewer than 8 characters"}; +if(func2(data35) < 8){ +const err118 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/minLength",keyword:"minLength",params:{limit: 8},message:"must NOT have fewer than 8 characters"}; if(vErrors === null){ -vErrors = [err89]; +vErrors = [err118]; } else { -vErrors.push(err89); +vErrors.push(err118); } errors++; } -if(!pattern4.test(data25)){ -const err90 = {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(!pattern4.test(data35)){ +const err119 = {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 = [err90]; +vErrors = [err119]; } else { -vErrors.push(err90); +vErrors.push(err119); } errors++; } } else { -const err91 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/type",keyword:"type",params:{type: "string"},message:"must be string"}; +const err120 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ -vErrors = [err91]; +vErrors = [err120]; } else { -vErrors.push(err91); +vErrors.push(err120); } errors++; } } if(data.kind !== undefined){ -let data26 = data.kind; -if(typeof data26 !== "string"){ -const err92 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/4/properties/kind/type",keyword:"type",params:{type: "string"},message:"must be string"}; +let data36 = data.kind; +if(typeof data36 !== "string"){ +const err121 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/4/properties/kind/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ -vErrors = [err92]; +vErrors = [err121]; } else { -vErrors.push(err92); +vErrors.push(err121); } errors++; } -if(!(data26 === "component")){ -const err93 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/4/properties/kind/enum",keyword:"enum",params:{allowedValues: schema166.oneOf[4].properties.kind.enum},message:"must be equal to one of the allowed values"}; +if(!(data36 === "component")){ +const err122 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/4/properties/kind/enum",keyword:"enum",params:{allowedValues: schema178.oneOf[4].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ -vErrors = [err93]; +vErrors = [err122]; } else { -vErrors.push(err93); +vErrors.push(err122); } errors++; } } if(data.type !== undefined){ -let data27 = data.type; -if(typeof data27 === "string"){ -if(!pattern51.test(data27)){ -const err94 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/4/properties/type/pattern",keyword:"pattern",params:{pattern: "^[a-z][a-z0-9-]{1,40}$"},message:"must match pattern \""+"^[a-z][a-z0-9-]{1,40}$"+"\""}; +let data37 = data.type; +if(typeof data37 === "string"){ +if(!pattern58.test(data37)){ +const err123 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/4/properties/type/pattern",keyword:"pattern",params:{pattern: "^[a-z][a-z0-9-]{1,40}$"},message:"must match pattern \""+"^[a-z][a-z0-9-]{1,40}$"+"\""}; if(vErrors === null){ -vErrors = [err94]; +vErrors = [err123]; } else { -vErrors.push(err94); +vErrors.push(err123); } errors++; } } else { -const err95 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/4/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; +const err124 = {instancePath:instancePath+"/type",schemaPath:"#/oneOf/4/properties/type/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ -vErrors = [err95]; +vErrors = [err124]; } else { -vErrors.push(err95); +vErrors.push(err124); } errors++; } } if(data.props !== undefined){ -let data28 = data.props; -if(data28 && typeof data28 == "object" && !Array.isArray(data28)){ +let data38 = data.props; +if(data38 && typeof data38 == "object" && !Array.isArray(data38)){ } else { -const err96 = {instancePath:instancePath+"/props",schemaPath:"#/oneOf/4/properties/props/type",keyword:"type",params:{type: "object"},message:"must be object"}; +const err125 = {instancePath:instancePath+"/props",schemaPath:"#/oneOf/4/properties/props/type",keyword:"type",params:{type: "object"},message:"must be object"}; if(vErrors === null){ -vErrors = [err96]; +vErrors = [err125]; } else { -vErrors.push(err96); +vErrors.push(err125); } errors++; } } } else { -const err97 = {instancePath,schemaPath:"#/oneOf/4/type",keyword:"type",params:{type: "object"},message:"must be object"}; +const err126 = {instancePath,schemaPath:"#/oneOf/4/type",keyword:"type",params:{type: "object"},message:"must be object"}; if(vErrors === null){ -vErrors = [err97]; +vErrors = [err126]; } else { -vErrors.push(err97); +vErrors.push(err126); } errors++; } -var _valid0 = _errs68 === errors; +var _valid0 = _errs92 === errors; if(_valid0 && valid0){ valid0 = false; passing0 = [passing0, 4]; @@ -14380,222 +16334,222 @@ if(props0 !== true){ props0 = true; } } -const _errs81 = errors; +const _errs105 = errors; if(data && typeof data == "object" && !Array.isArray(data)){ if(data.id === undefined){ -const err98 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "id"},message:"must have required property '"+"id"+"'"}; +const err127 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "id"},message:"must have required property '"+"id"+"'"}; if(vErrors === null){ -vErrors = [err98]; +vErrors = [err127]; } else { -vErrors.push(err98); +vErrors.push(err127); } errors++; } if(data.kind === undefined){ -const err99 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "kind"},message:"must have required property '"+"kind"+"'"}; +const err128 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "kind"},message:"must have required property '"+"kind"+"'"}; if(vErrors === null){ -vErrors = [err99]; +vErrors = [err128]; } else { -vErrors.push(err99); +vErrors.push(err128); } errors++; } if(data.catalog === undefined){ -const err100 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "catalog"},message:"must have required property '"+"catalog"+"'"}; +const err129 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "catalog"},message:"must have required property '"+"catalog"+"'"}; if(vErrors === null){ -vErrors = [err100]; +vErrors = [err129]; } else { -vErrors.push(err100); +vErrors.push(err129); } errors++; } if(data.components === undefined){ -const err101 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "components"},message:"must have required property '"+"components"+"'"}; +const err130 = {instancePath,schemaPath:"#/oneOf/5/required",keyword:"required",params:{missingProperty: "components"},message:"must have required property '"+"components"+"'"}; if(vErrors === null){ -vErrors = [err101]; +vErrors = [err130]; } else { -vErrors.push(err101); +vErrors.push(err130); } errors++; } -for(const key6 in data){ -if(!(((((key6 === "id") || (key6 === "kind")) || (key6 === "catalog")) || (key6 === "components")) || (key6 === "data"))){ -const err102 = {instancePath,schemaPath:"#/oneOf/5/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key6},message:"must NOT have additional properties"}; +for(const key8 in data){ +if(!(((((key8 === "id") || (key8 === "kind")) || (key8 === "catalog")) || (key8 === "components")) || (key8 === "data"))){ +const err131 = {instancePath,schemaPath:"#/oneOf/5/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key8},message:"must NOT have additional properties"}; if(vErrors === null){ -vErrors = [err102]; +vErrors = [err131]; } else { -vErrors.push(err102); +vErrors.push(err131); } errors++; } } if(data.id !== undefined){ -let data29 = data.id; -if(typeof data29 === "string"){ -if(func2(data29) > 48){ -const err103 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/maxLength",keyword:"maxLength",params:{limit: 48},message:"must NOT have more than 48 characters"}; +let data39 = data.id; +if(typeof data39 === "string"){ +if(func2(data39) > 48){ +const err132 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/maxLength",keyword:"maxLength",params:{limit: 48},message:"must NOT have more than 48 characters"}; if(vErrors === null){ -vErrors = [err103]; +vErrors = [err132]; } else { -vErrors.push(err103); +vErrors.push(err132); } errors++; } -if(func2(data29) < 8){ -const err104 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/minLength",keyword:"minLength",params:{limit: 8},message:"must NOT have fewer than 8 characters"}; +if(func2(data39) < 8){ +const err133 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/minLength",keyword:"minLength",params:{limit: 8},message:"must NOT have fewer than 8 characters"}; if(vErrors === null){ -vErrors = [err104]; +vErrors = [err133]; } else { -vErrors.push(err104); +vErrors.push(err133); } errors++; } -if(!pattern4.test(data29)){ -const err105 = {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(!pattern4.test(data39)){ +const err134 = {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 = [err105]; +vErrors = [err134]; } else { -vErrors.push(err105); +vErrors.push(err134); } errors++; } } else { -const err106 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/type",keyword:"type",params:{type: "string"},message:"must be string"}; +const err135 = {instancePath:instancePath+"/id",schemaPath:"#/$defs/Id/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ -vErrors = [err106]; +vErrors = [err135]; } else { -vErrors.push(err106); +vErrors.push(err135); } errors++; } } if(data.kind !== undefined){ -let data30 = data.kind; -if(typeof data30 !== "string"){ -const err107 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/5/properties/kind/type",keyword:"type",params:{type: "string"},message:"must be string"}; +let data40 = data.kind; +if(typeof data40 !== "string"){ +const err136 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/5/properties/kind/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ -vErrors = [err107]; +vErrors = [err136]; } else { -vErrors.push(err107); +vErrors.push(err136); } errors++; } -if(!(data30 === "surface")){ -const err108 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/5/properties/kind/enum",keyword:"enum",params:{allowedValues: schema166.oneOf[5].properties.kind.enum},message:"must be equal to one of the allowed values"}; +if(!(data40 === "surface")){ +const err137 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/5/properties/kind/enum",keyword:"enum",params:{allowedValues: schema178.oneOf[5].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ -vErrors = [err108]; +vErrors = [err137]; } else { -vErrors.push(err108); +vErrors.push(err137); } errors++; } } if(data.catalog !== undefined){ -let data31 = data.catalog; -if(typeof data31 === "string"){ -if(!pattern53.test(data31)){ -const err109 = {instancePath:instancePath+"/catalog",schemaPath:"#/oneOf/5/properties/catalog/pattern",keyword:"pattern",params:{pattern: "^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"},message:"must match pattern \""+"^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"+"\""}; +let data41 = data.catalog; +if(typeof data41 === "string"){ +if(!pattern60.test(data41)){ +const err138 = {instancePath:instancePath+"/catalog",schemaPath:"#/oneOf/5/properties/catalog/pattern",keyword:"pattern",params:{pattern: "^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"},message:"must match pattern \""+"^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"+"\""}; if(vErrors === null){ -vErrors = [err109]; +vErrors = [err138]; } else { -vErrors.push(err109); +vErrors.push(err138); } errors++; } } else { -const err110 = {instancePath:instancePath+"/catalog",schemaPath:"#/oneOf/5/properties/catalog/type",keyword:"type",params:{type: "string"},message:"must be string"}; +const err139 = {instancePath:instancePath+"/catalog",schemaPath:"#/oneOf/5/properties/catalog/type",keyword:"type",params:{type: "string"},message:"must be string"}; if(vErrors === null){ -vErrors = [err110]; +vErrors = [err139]; } else { -vErrors.push(err110); +vErrors.push(err139); } errors++; } } if(data.components !== undefined){ -let data32 = data.components; -if(Array.isArray(data32)){ -if(data32.length > 200){ -const err111 = {instancePath:instancePath+"/components",schemaPath:"#/oneOf/5/properties/components/maxItems",keyword:"maxItems",params:{limit: 200},message:"must NOT have more than 200 items"}; +let data42 = data.components; +if(Array.isArray(data42)){ +if(data42.length > 200){ +const err140 = {instancePath:instancePath+"/components",schemaPath:"#/oneOf/5/properties/components/maxItems",keyword:"maxItems",params:{limit: 200},message:"must NOT have more than 200 items"}; if(vErrors === null){ -vErrors = [err111]; +vErrors = [err140]; } else { -vErrors.push(err111); +vErrors.push(err140); } errors++; } -if(data32.length < 1){ -const err112 = {instancePath:instancePath+"/components",schemaPath:"#/oneOf/5/properties/components/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}; +if(data42.length < 1){ +const err141 = {instancePath:instancePath+"/components",schemaPath:"#/oneOf/5/properties/components/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"}; if(vErrors === null){ -vErrors = [err112]; +vErrors = [err141]; } else { -vErrors.push(err112); +vErrors.push(err141); } errors++; } -const len0 = data32.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"]}; +export const validateContentEntry = validate119; +const schema201 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://cloudhal.dev/schemas/v1/content-entry.schema.json","title":"ContentEntry","description":"A structured content item in a named collection (e.g. collection 'posts'). Core fields are typed; collection-specific fields live in `fields` and are validated by the collection's own schema in a later phase.","type":"object","additionalProperties":false,"required":["schemaVersion","id","siteId","collection","slug","locale","status","title","body","fields","tags","author","rev","createdAt","updatedAt"],"properties":{"schemaVersion":{"$ref":"#/$defs/SchemaVersion"},"id":{"$ref":"#/$defs/Id"},"siteId":{"$ref":"#/$defs/Id"},"collection":{"$ref":"#/$defs/Slug"},"slug":{"$ref":"#/$defs/Slug"},"locale":{"$ref":"#/$defs/Locale"},"status":{"$ref":"#/$defs/PublishStatus"},"title":{"type":"string","minLength":1,"maxLength":300},"summary":{"type":"string","maxLength":2000},"body":{"$ref":"#/$defs/Body"},"fields":{"type":"object","description":"Collection-specific values, keyed by field. A collection with typed fields (RFC-0041, Site.collections..fields) checks each value against its definition on write; otherwise any JSON. An absent key means no value.","additionalProperties":true},"tags":{"type":"array","description":"Deprecated by RFC-0002: use `terms.tag`. Still accepted on input for one release and translated into terms; the store then writes []. Removed in schemaVersion 2.","deprecated":true,"items":{"$ref":"#/$defs/Slug"},"uniqueItems":true,"maxItems":50},"terms":{"type":"object","description":"Term assignments by taxonomy key (e.g. 'category', 'tag'). Terms are referenced by id, independent of locale. A taxonomy without terms is omitted, never an empty object.","propertyNames":{"$ref":"#/$defs/Slug"},"minProperties":1,"maxProperties":10,"additionalProperties":{"$ref":"#/$defs/TermAssignment"}},"author":{"$ref":"#/$defs/Actor"},"coverImage":{"$ref":"#/$defs/ImageRef"},"publishedAt":{"$ref":"#/$defs/Timestamp"},"availability":{"$ref":"#/$defs/Availability"},"rev":{"$ref":"#/$defs/Revision"},"createdAt":{"$ref":"#/$defs/Timestamp"},"updatedAt":{"$ref":"#/$defs/Timestamp"},"translation":{"$ref":"#/$defs/Translation"},"metadata":{"$ref":"#/$defs/Metadata"},"corrections":{"type":"array","description":"Corrections, clarifications and updates appended to the entry, oldest first (RFC-0019 §2). The store stamps `at`; an item that has been live can no longer be removed, and only a person may edit its note.","items":{"$ref":"#/$defs/Correction"},"maxItems":100}},"$defs":{"Actor":{"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}},"ActorKind":{"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]},"Availability":{"type":"object","description":"Time gates of a page or content entry (RFC-0008): the published site shows the node only from `from` (inclusive: an embargo) until `until` (exclusive: an expiry), checked at render time, so the gates hold even when no scheduler runs. Before `from` and from `until` on, the node is absent from every public route, list, feed, sitemap and search output; previews still show it. At least one gate; writers check that `from` is before `until`.","additionalProperties":false,"minProperties":1,"properties":{"from":{"$ref":"#/$defs/Timestamp"},"until":{"$ref":"#/$defs/Timestamp"}}},"Body":{"type":"object","additionalProperties":false,"required":["format","value"],"properties":{"format":{"type":"string","enum":["markdown","html"]},"value":{"type":"string","maxLength":1000000},"profile":{"$ref":"#/$defs/BodyProfile"},"blocks":{"$ref":"#/$defs/BodyBlocks"}}},"BodyBlock":{"type":"object","description":"One top-level block of a markdown body in the store's index (RFC-0014 §3). Written by the store only: a client's values are replaced on every write. `by`, `via` and `at` say who last changed the block's content, through which channel and when; they are absent for content written before the index existed.","additionalProperties":false,"required":["id","hash"],"properties":{"id":{"type":"string","pattern":"^blk_[0-9A-Za-z]{8,24}$","description":"Stable across writes that keep the block's content or its place among unchanged blocks."},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"SHA-256 (hex) of the block's markdown without trailing whitespace."},"by":{"$ref":"#/$defs/Actor"},"via":{"type":"string","enum":["studio","mcp","api","import","ai-command","proposal"],"description":"The channel of that change."},"at":{"$ref":"#/$defs/Timestamp"}}},"BodyBlocks":{"type":"array","description":"A markdown body's block index, in body order (RFC-0014 §3). Absent means not indexed (yet, or an HTML body), never empty.","maxItems":5000,"items":{"$ref":"#/$defs/BodyBlock"}},"BodyProfile":{"type":"integer","enum":[1],"description":"The markdown dialect a body follows (RFC-0014 §1): 1 = Cloudhal Markdown v1 (CommonMark + GFM + `asset:` + directives; raw HTML escaped). Set by the store on markdown bodies."},"Correction":{"type":"object","description":"One correction notice (RFC-0019 §2).","additionalProperties":false,"required":["id","kind","at","note"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["correction","clarification","update"]},"at":{"$ref":"#/$defs/Timestamp","description":"When it was written; set by the store, never by the caller."},"note":{"type":"string","description":"What changed, in markdown (inline).","minLength":1,"maxLength":2000},"snapshotId":{"$ref":"#/$defs/Id","description":"The snapshot it was first published in."}}},"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"ImageRef":{"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}},"Locale":{"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"},"Metadata":{"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}},"PublishStatus":{"type":"string","description":"Editorial lifecycle of a page or content entry.","enum":["draft","review","published","archived"]},"Revision":{"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0},"SchemaVersion":{"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]},"Slug":{"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128},"TermAssignment":{"type":"object","description":"Terms of one taxonomy on one entry, in display order. The primary term is explicit and never derived from the order.","additionalProperties":false,"required":["ids"],"properties":{"ids":{"type":"array","items":{"$ref":"#/$defs/Id"},"minItems":1,"maxItems":50,"uniqueItems":true},"primary":{"$ref":"#/$defs/Id","description":"The entry's main term in this taxonomy (breadcrumbs, articleSection, later permalinks). Must be one of `ids`."}}},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"},"Translation":{"type":"object","description":"Links a page or entry to the other language versions of the same work (RFC-0033 §2). Staleness is computed: the source's current hash differs from source.hash.","additionalProperties":false,"required":["group","method"],"properties":{"group":{"$ref":"#/$defs/Id","description":"The group's id: the original node's id, shared by every translation. May cross sites in one project."},"method":{"$ref":"#/$defs/TranslationMethod"},"layout":{"type":"string","description":"linked: the original's structural changes propagate to this translation as drafts (RFC-0033 §4a); independent: nothing propagates. Absent means linked.","enum":["linked","independent"]},"source":{"type":"object","description":"The node and revision this translation was made from. Absent on the original.","additionalProperties":false,"required":["id","locale","rev","hash"],"properties":{"id":{"$ref":"#/$defs/Id"},"siteId":{"$ref":"#/$defs/Id","description":"The source's site when it is another site of the same project (a language edition). Absent means this site."},"locale":{"$ref":"#/$defs/Locale"},"rev":{"$ref":"#/$defs/Revision"},"hash":{"type":"string","description":"Lower-case hex SHA-256 of the source node's canonical JSON at that revision.","pattern":"^[0-9a-f]{64}$"}}},"provenance":{"type":"object","description":"The machine-translation record: engine, glossary revision, checks run, quality score. Required when method is machine or machine-reviewed.","additionalProperties":false,"required":["engine","at"],"properties":{"engine":{"type":"string","minLength":1,"maxLength":200},"glossaryRev":{"$ref":"#/$defs/Revision"},"score":{"type":"number","minimum":0,"maximum":100},"checks":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":50},"at":{"$ref":"#/$defs/Timestamp"}}},"review":{"type":"object","description":"The person who reviewed this translation and when. Only humans set it.","additionalProperties":false,"required":["by","at"],"properties":{"by":{"$ref":"#/$defs/Actor"},"at":{"$ref":"#/$defs/Timestamp"}}}}},"TranslationMethod":{"type":"string","description":"How a page's or entry's text came to be (RFC-0033). 'original' marks the source of a translation group; 'machine-reviewed' is machine output a person has reviewed.","enum":["original","human","machine","machine-reviewed"]}}}; +const schema202 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; +const schema203 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; +const schema205 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; +const schema207 = {"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"}; +const schema208 = {"type":"string","description":"Editorial lifecycle of a page or content entry.","enum":["draft","review","published","archived"]}; +const schema216 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema228 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; +const schema241 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; +const schema209 = {"type":"object","additionalProperties":false,"required":["format","value"],"properties":{"format":{"type":"string","enum":["markdown","html"]},"value":{"type":"string","maxLength":1000000},"profile":{"$ref":"#/$defs/BodyProfile"},"blocks":{"$ref":"#/$defs/BodyBlocks"}}}; +const schema210 = {"type":"integer","enum":[1],"description":"The markdown dialect a body follows (RFC-0014 §1): 1 = Cloudhal Markdown v1 (CommonMark + GFM + `asset:` + directives; raw HTML escaped). Set by the store on markdown bodies."}; +const schema211 = {"type":"array","description":"A markdown body's block index, in body order (RFC-0014 §3). Absent means not indexed (yet, or an HTML body), never empty.","maxItems":5000,"items":{"$ref":"#/$defs/BodyBlock"}}; +const schema212 = {"type":"object","description":"One top-level block of a markdown body in the store's index (RFC-0014 §3). Written by the store only: a client's values are replaced on every write. `by`, `via` and `at` say who last changed the block's content, through which channel and when; they are absent for content written before the index existed.","additionalProperties":false,"required":["id","hash"],"properties":{"id":{"type":"string","pattern":"^blk_[0-9A-Za-z]{8,24}$","description":"Stable across writes that keep the block's content or its place among unchanged blocks."},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"SHA-256 (hex) of the block's markdown without trailing whitespace."},"by":{"$ref":"#/$defs/Actor"},"via":{"type":"string","enum":["studio","mcp","api","import","ai-command","proposal"],"description":"The channel of that change."},"at":{"$ref":"#/$defs/Timestamp"}}}; +const schema213 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; +const schema214 = {"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]}; -function validate117(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate123(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate117.evaluated; +const evaluated0 = validate123.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -14713,7 +16667,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: schema200.enum},message:"must be equal to one of the allowed values"}; +const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema214.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -14804,16 +16758,16 @@ vErrors.push(err11); } errors++; } -validate117.errors = vErrors; +validate123.errors = vErrors; return errors === 0; } -validate117.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate123.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate116(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate122(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate116.evaluated; +const evaluated0 = validate122.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -14856,7 +16810,7 @@ errors++; if(data.id !== undefined){ let data0 = data.id; if(typeof data0 === "string"){ -if(!pattern44.test(data0)){ +if(!pattern49.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]; @@ -14881,7 +16835,7 @@ errors++; if(data.hash !== undefined){ let data1 = data.hash; if(typeof data1 === "string"){ -if(!pattern45.test(data1)){ +if(!pattern50.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]; @@ -14904,8 +16858,8 @@ errors++; } } if(data.by !== undefined){ -if(!(validate117(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate117.errors : vErrors.concat(validate117.errors); +if(!(validate123(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate123.errors : vErrors.concat(validate123.errors); errors = vErrors.length; } } @@ -14922,7 +16876,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: schema198.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: schema212.properties.via.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err8]; } @@ -14968,16 +16922,16 @@ vErrors.push(err11); } errors++; } -validate116.errors = vErrors; +validate122.errors = vErrors; return errors === 0; } -validate116.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate122.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate115(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate121(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate115.evaluated; +const evaluated0 = validate121.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -14997,8 +16951,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 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}; +export const validateTaxonomy = validate146; +const schema263 = {"$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 schema264 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; +const schema265 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; +const schema267 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; +const schema268 = {"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"}; +const schema269 = {"type":"object","additionalProperties":false,"required":["singular","plural"],"properties":{"singular":{"type":"string","minLength":1,"maxLength":100},"plural":{"type":"string","minLength":1,"maxLength":100},"description":{"type":"string","maxLength":1000}}}; +const schema273 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; +const schema274 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema276 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; +const schema271 = {"type":"object","description":"Public archive pages: / and //page/.","additionalProperties":false,"required":["enabled","basePath"],"properties":{"enabled":{"type":"boolean"},"basePath":{"$ref":"#/$defs/Path"}}}; +const schema272 = {"type":"string","description":"Absolute URL path of a route the site owns: starts with '/', lower-case slug segments, no trailing slash except root.","pattern":"^/(?:[a-z0-9]+(?:-[a-z0-9]+)*(?:/[a-z0-9]+(?:-[a-z0-9]+)*)*)?$","maxLength":1024}; -function validate141(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate147(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate141.evaluated; +const evaluated0 = validate147.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -18771,7 +20725,7 @@ vErrors.push(err4); } errors++; } -if(!pattern58.test(data1)){ +if(!pattern65.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]; @@ -18804,17 +20758,17 @@ vErrors.push(err7); } errors++; } -validate141.errors = vErrors; +validate147.errors = vErrors; return errors === 0; } -validate141.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate147.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate140(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate146(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ /*# sourceURL="https://cloudhal.dev/schemas/v1/taxonomy.schema.json" */; let vErrors = null; let errors = 0; -const evaluated0 = validate140.evaluated; +const evaluated0 = validate146.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -18963,7 +20917,7 @@ vErrors.push(err13); errors++; } for(const key0 in data){ -if(!(func1.call(schema249.properties, key0))){ +if(!(func1.call(schema263.properties, key0))){ const err14 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}; if(vErrors === null){ vErrors = [err14]; @@ -18987,7 +20941,7 @@ vErrors.push(err15); errors++; } if(!(data0 === 1)){ -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"}; +const err16 = {instancePath:instancePath+"/schemaVersion",schemaPath:"#/$defs/SchemaVersion/enum",keyword:"enum",params:{allowedValues: schema264.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err16]; } @@ -19360,7 +21314,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: schema249.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: schema263.properties.allowNewTerms.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err48]; } @@ -19519,8 +21473,8 @@ errors++; } } if(data.archive !== undefined){ -if(!(validate141(data.archive, {instancePath:instancePath+"/archive",parentData:data,parentDataProperty:"archive",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate141.errors : vErrors.concat(validate141.errors); +if(!(validate147(data.archive, {instancePath:instancePath+"/archive",parentData:data,parentDataProperty:"archive",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate147.errors : vErrors.concat(validate147.errors); errors = vErrors.length; } } @@ -19684,29 +21638,29 @@ vErrors.push(err73); } errors++; } -validate140.errors = vErrors; +validate146.errors = vErrors; return errors === 0; } -validate140.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate146.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -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}}}; +export const validateTerm = validate149; +const schema277 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://cloudhal.dev/schemas/v1/term.schema.json","title":"Term","description":"One concept in a taxonomy (a category, a tag, a series). Content refers to terms by id, independent of locale; names, synonyms, archive intro and SEO are per locale. Renames, merges and retirements keep the id and leave redirects behind; `proposedBy` records the actor that proposed it (RFC-0002).","type":"object","additionalProperties":false,"required":["schemaVersion","id","siteId","taxonomyId","slug","status","visibility","labels","rev","createdAt","updatedAt"],"properties":{"schemaVersion":{"$ref":"#/$defs/SchemaVersion"},"id":{"$ref":"#/$defs/Id"},"siteId":{"$ref":"#/$defs/Id"},"taxonomyId":{"$ref":"#/$defs/Id"},"slug":{"$ref":"#/$defs/Slug","description":"URL segment of the term's archive; unique within its taxonomy, together with every slug in any term's slugHistory."},"status":{"type":"string","description":"active: usable and public (subject to visibility). proposed: created by an agent, waiting for a person; assignable but never public. retired: no new assignments, no archive.","enum":["active","proposed","retired"]},"visibility":{"type":"string","description":"internal terms have no archive, feed or sitemap entry and are not shown on entries; themes and filters may still use them.","enum":["public","internal"]},"labels":{"type":"object","description":"Names per locale. Must contain the site's default locale.","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"$ref":"#/$defs/TermLabel"}},"parentId":{"$ref":"#/$defs/Id","description":"Parent term in the same taxonomy; only in hierarchical taxonomies. No cycles."},"position":{"type":"integer","description":"Order among sibling terms (same taxonomy and parent), lowest first, for menus and sidebars. Terms without a position follow, by name.","minimum":0,"maximum":100000},"image":{"$ref":"#/$defs/ImageRef"},"links":{"type":"array","description":"Profile links shown with the term and given to search engines as sameAs, such as an author's pages elsewhere (RFC-0019 §1).","maxItems":10,"items":{"$ref":"#/$defs/Link"}},"slugHistory":{"type":"array","description":"Earlier slugs, oldest first. Old slugs stay reserved in the taxonomy and still resolve when an agent assigns by slug.","items":{"$ref":"#/$defs/Slug"},"minItems":1,"maxItems":50,"uniqueItems":true},"mergedInto":{"$ref":"#/$defs/Id","description":"Set when this term was merged into another active term of the same taxonomy; status is then 'retired'."},"proposedBy":{"$ref":"#/$defs/Actor"},"rev":{"$ref":"#/$defs/Revision"},"createdAt":{"$ref":"#/$defs/Timestamp"},"updatedAt":{"$ref":"#/$defs/Timestamp"},"metadata":{"$ref":"#/$defs/Metadata"}},"$defs":{"Actor":{"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}},"ActorKind":{"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]},"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"ImageRef":{"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}},"Link":{"type":"object","additionalProperties":false,"required":["label","href"],"properties":{"label":{"type":"string","minLength":1,"maxLength":200},"href":{"type":"string","minLength":1,"maxLength":2048},"external":{"type":"boolean"}}},"Locale":{"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"},"Metadata":{"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}},"Revision":{"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0},"SchemaVersion":{"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]},"Seo":{"type":"object","description":"Search and social metadata of a routable node (page, term archive).","additionalProperties":false,"properties":{"title":{"type":"string","maxLength":300},"description":{"type":"string","maxLength":500},"noindex":{"type":"boolean"},"canonical":{"type":"string","maxLength":2048},"ogImage":{"$ref":"#/$defs/ImageRef"}}},"Slug":{"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128},"TermLabel":{"type":"object","additionalProperties":false,"required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":200},"slug":{"$ref":"#/$defs/Slug","description":"This locale's archive slug (RFC-0033); absent uses the term's slug. Never set for the site's default locale, whose slug is the term's own; unique per taxonomy and locale."},"altNames":{"type":"array","description":"Synonyms (SKOS altLabel). Matched when an agent or editor assigns by name; never shown as separate terms.","items":{"type":"string","minLength":1,"maxLength":200},"minItems":1,"maxItems":50,"uniqueItems":true},"description":{"type":"string","description":"Markdown introduction shown at the top of the archive.","maxLength":20000},"role":{"type":"string","description":"A short role or title shown with the term, such as an author's \"Senior reporter\" (RFC-0019 §1).","minLength":1,"maxLength":120},"seo":{"$ref":"#/$defs/Seo"}}},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}}}; +const schema278 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; +const schema279 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; +const schema282 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; +const schema283 = {"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"}; +const schema290 = {"type":"object","additionalProperties":false,"required":["label","href"],"properties":{"label":{"type":"string","minLength":1,"maxLength":200},"href":{"type":"string","minLength":1,"maxLength":2048},"external":{"type":"boolean"}}}; +const schema296 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; +const schema297 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema299 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; +const schema284 = {"type":"object","additionalProperties":false,"required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":200},"slug":{"$ref":"#/$defs/Slug","description":"This locale's archive slug (RFC-0033); absent uses the term's slug. Never set for the site's default locale, whose slug is the term's own; unique per taxonomy and locale."},"altNames":{"type":"array","description":"Synonyms (SKOS altLabel). Matched when an agent or editor assigns by name; never shown as separate terms.","items":{"type":"string","minLength":1,"maxLength":200},"minItems":1,"maxItems":50,"uniqueItems":true},"description":{"type":"string","description":"Markdown introduction shown at the top of the archive.","maxLength":20000},"role":{"type":"string","description":"A short role or title shown with the term, such as an author's \"Senior reporter\" (RFC-0019 §1).","minLength":1,"maxLength":120},"seo":{"$ref":"#/$defs/Seo"}}}; +const schema286 = {"type":"object","description":"Search and social metadata of a routable node (page, term archive).","additionalProperties":false,"properties":{"title":{"type":"string","maxLength":300},"description":{"type":"string","maxLength":500},"noindex":{"type":"boolean"},"canonical":{"type":"string","maxLength":2048},"ogImage":{"$ref":"#/$defs/ImageRef"}}}; +const schema287 = {"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}}; -function validate146(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate152(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate146.evaluated; +const evaluated0 = validate152.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -19912,16 +21866,16 @@ vErrors.push(err16); } errors++; } -validate146.errors = vErrors; +validate152.errors = vErrors; return errors === 0; } -validate146.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate152.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate145(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate151(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate145.evaluated; +const evaluated0 = validate151.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -20029,8 +21983,8 @@ errors++; } } if(data.ogImage !== undefined){ -if(!(validate146(data.ogImage, {instancePath:instancePath+"/ogImage",parentData:data,parentDataProperty:"ogImage",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate146.errors : vErrors.concat(validate146.errors); +if(!(validate152(data.ogImage, {instancePath:instancePath+"/ogImage",parentData:data,parentDataProperty:"ogImage",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate152.errors : vErrors.concat(validate152.errors); errors = vErrors.length; } } @@ -20045,16 +21999,16 @@ vErrors.push(err8); } errors++; } -validate145.errors = vErrors; +validate151.errors = vErrors; return errors === 0; } -validate145.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate151.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate144(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate150(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate144.evaluated; +const evaluated0 = validate150.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -20320,8 +22274,8 @@ errors++; } } if(data.seo !== undefined){ -if(!(validate145(data.seo, {instancePath:instancePath+"/seo",parentData:data,parentDataProperty:"seo",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate145.errors : vErrors.concat(validate145.errors); +if(!(validate151(data.seo, {instancePath:instancePath+"/seo",parentData:data,parentDataProperty:"seo",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate151.errors : vErrors.concat(validate151.errors); errors = vErrors.length; } } @@ -20336,18 +22290,18 @@ vErrors.push(err21); } errors++; } -validate144.errors = vErrors; +validate150.errors = vErrors; return errors === 0; } -validate144.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate150.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -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"]}; +const schema293 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; +const schema294 = {"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]}; -function validate151(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate157(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate151.evaluated; +const evaluated0 = validate157.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -20400,7 +22354,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: schema280.enum},message:"must be equal to one of the allowed values"}; +const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema294.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -20491,17 +22445,17 @@ vErrors.push(err11); } errors++; } -validate151.errors = vErrors; +validate157.errors = vErrors; return errors === 0; } -validate151.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate157.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate143(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate149(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ /*# sourceURL="https://cloudhal.dev/schemas/v1/term.schema.json" */; let vErrors = null; let errors = 0; -const evaluated0 = validate143.evaluated; +const evaluated0 = validate149.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -20620,7 +22574,7 @@ vErrors.push(err10); errors++; } for(const key0 in data){ -if(!(func1.call(schema263.properties, key0))){ +if(!(func1.call(schema277.properties, key0))){ const err11 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}; if(vErrors === null){ vErrors = [err11]; @@ -20644,7 +22598,7 @@ vErrors.push(err12); errors++; } if(!(data0 === 1)){ -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"}; +const err13 = {instancePath:instancePath+"/schemaVersion",schemaPath:"#/$defs/SchemaVersion/enum",keyword:"enum",params:{allowedValues: schema278.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err13]; } @@ -20847,7 +22801,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: schema263.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: schema277.properties.status.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err31]; } @@ -20870,7 +22824,7 @@ vErrors.push(err32); errors++; } if(!((data6 === "public") || (data6 === "internal"))){ -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"}; +const err33 = {instancePath:instancePath+"/visibility",schemaPath:"#/properties/visibility/enum",keyword:"enum",params:{allowedValues: schema277.properties.visibility.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err33]; } @@ -20930,8 +22884,8 @@ errors++; } } for(const key2 in data7){ -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); +if(!(validate150(data7[key2], {instancePath:instancePath+"/labels/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),parentData:data7,parentDataProperty:key2,rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate150.errors : vErrors.concat(validate150.errors); errors = vErrors.length; } } @@ -21028,8 +22982,8 @@ errors++; } } if(data.image !== undefined){ -if(!(validate146(data.image, {instancePath:instancePath+"/image",parentData:data,parentDataProperty:"image",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate146.errors : vErrors.concat(validate146.errors); +if(!(validate152(data.image, {instancePath:instancePath+"/image",parentData:data,parentDataProperty:"image",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate152.errors : vErrors.concat(validate152.errors); errors = vErrors.length; } } @@ -21335,8 +23289,8 @@ errors++; } } if(data.proposedBy !== undefined){ -if(!(validate151(data.proposedBy, {instancePath:instancePath+"/proposedBy",parentData:data,parentDataProperty:"proposedBy",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate151.errors : vErrors.concat(validate151.errors); +if(!(validate157(data.proposedBy, {instancePath:instancePath+"/proposedBy",parentData:data,parentDataProperty:"proposedBy",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate157.errors : vErrors.concat(validate157.errors); errors = vErrors.length; } } @@ -21453,27 +23407,27 @@ vErrors.push(err79); } errors++; } -validate143.errors = vErrors; +validate149.errors = vErrors; return errors === 0; } -validate143.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate149.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -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"}}}; +export const validateRedirect = validate159; +const schema300 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://cloudhal.dev/schemas/v1/redirect.schema.json","title":"Redirect","description":"An HTTP redirect from a path the site no longer serves. Created automatically when a published entry or a term changes slug, when terms are merged or retired, and by people. Snapshotted and published like every other node (RFC-0002).","type":"object","additionalProperties":false,"required":["schemaVersion","id","siteId","from","to","status","match","reason","rev","createdAt","updatedAt"],"properties":{"schemaVersion":{"$ref":"#/$defs/SchemaVersion"},"id":{"$ref":"#/$defs/Id"},"siteId":{"$ref":"#/$defs/Id"},"from":{"type":"string","description":"Request path to redirect: starts with '/', no query, fragment, backslash or trailing slash (root excepted). Unique per site and locale; never a path the site currently serves.","pattern":"^/(?:[^\\s?#/\\\\]+(?:/[^\\s?#/\\\\]+)*)?$","maxLength":1024},"locale":{"$ref":"#/$defs/Locale","description":"The locale whose URLs `from` belongs to (RFC-0033); absent means the site's default locale. `from` is the path inside that locale, without its prefix or domain. Unique per site and locale."},"to":{"type":"string","description":"Target: a site path (may carry a query or fragment) or an absolute https URL. Protocol-relative ('//host') and backslash forms are rejected.","pattern":"^(?:/(?![/\\\\])[^\\s\\\\]*|https://[^\\s\\\\/?#]+(?:[/?#][^\\s\\\\]*)?)$","maxLength":2048},"status":{"type":"integer","description":"301 (permanent) for slug changes, merges and retirements; 302 (temporary) only for manual redirects.","enum":[301,302]},"match":{"type":"string","description":"exact: only `from` itself. prefix: `from` and every path below it; the rest of the path is appended to `to` (archives: /tag/old/page/2 → /tag/new/page/2).","enum":["exact","prefix"]},"reason":{"type":"string","enum":["slug-change","merge","retire","manual"]},"source":{"$ref":"#/$defs/RedirectSource"},"note":{"type":"string","maxLength":500},"rev":{"$ref":"#/$defs/Revision"},"createdAt":{"$ref":"#/$defs/Timestamp"},"updatedAt":{"$ref":"#/$defs/Timestamp"},"metadata":{"$ref":"#/$defs/Metadata"}},"$defs":{"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"Locale":{"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"},"Metadata":{"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}},"RedirectSource":{"type":"object","description":"The node whose earlier URL this redirect preserves (automatic redirects). Used to collapse chains on later renames and to list a node's old URLs.","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"type":"string","enum":["page","contentEntry","term"]},"id":{"$ref":"#/$defs/Id"}}},"Revision":{"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0},"SchemaVersion":{"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}}}; +const schema301 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; +const schema302 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; +const schema304 = {"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"}; +const schema307 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; +const schema308 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema310 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; +const pattern159 = new RegExp("^/(?:[^\\s?#/\\\\]+(?:/[^\\s?#/\\\\]+)*)?$", "u"); +const pattern161 = new RegExp("^(?:/(?![/\\\\])[^\\s\\\\]*|https://[^\\s\\\\/?#]+(?:[/?#][^\\s\\\\]*)?)$", "u"); +const schema305 = {"type":"object","description":"The node whose earlier URL this redirect preserves (automatic redirects). Used to collapse chains on later renames and to list a node's old URLs.","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"type":"string","enum":["page","contentEntry","term"]},"id":{"$ref":"#/$defs/Id"}}}; -function validate154(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate160(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate154.evaluated; +const evaluated0 = validate160.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -21526,7 +23480,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: schema291.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: schema305.properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -21592,17 +23546,17 @@ vErrors.push(err9); } errors++; } -validate154.errors = vErrors; +validate160.errors = vErrors; return errors === 0; } -validate154.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate160.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate153(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate159(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ /*# sourceURL="https://cloudhal.dev/schemas/v1/redirect.schema.json" */; let vErrors = null; let errors = 0; -const evaluated0 = validate153.evaluated; +const evaluated0 = validate159.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -21721,7 +23675,7 @@ vErrors.push(err10); errors++; } for(const key0 in data){ -if(!(func1.call(schema286.properties, key0))){ +if(!(func1.call(schema300.properties, key0))){ const err11 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}; if(vErrors === null){ vErrors = [err11]; @@ -21745,7 +23699,7 @@ vErrors.push(err12); errors++; } if(!(data0 === 1)){ -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"}; +const err13 = {instancePath:instancePath+"/schemaVersion",schemaPath:"#/$defs/SchemaVersion/enum",keyword:"enum",params:{allowedValues: schema301.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err13]; } @@ -21858,7 +23812,7 @@ vErrors.push(err22); } errors++; } -if(!pattern148.test(data3)){ +if(!pattern159.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]; @@ -21918,7 +23872,7 @@ vErrors.push(err27); } errors++; } -if(!pattern150.test(data5)){ +if(!pattern161.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]; @@ -21953,7 +23907,7 @@ vErrors.push(err30); errors++; } if(!((data6 === 301) || (data6 === 302))){ -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"}; +const err31 = {instancePath:instancePath+"/status",schemaPath:"#/properties/status/enum",keyword:"enum",params:{allowedValues: schema300.properties.status.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err31]; } @@ -21976,7 +23930,7 @@ vErrors.push(err32); errors++; } if(!((data7 === "exact") || (data7 === "prefix"))){ -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"}; +const err33 = {instancePath:instancePath+"/match",schemaPath:"#/properties/match/enum",keyword:"enum",params:{allowedValues: schema300.properties.match.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err33]; } @@ -21999,7 +23953,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: schema286.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: schema300.properties.reason.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err35]; } @@ -22010,8 +23964,8 @@ errors++; } } if(data.source !== undefined){ -if(!(validate154(data.source, {instancePath:instancePath+"/source",parentData:data,parentDataProperty:"source",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate154.errors : vErrors.concat(validate154.errors); +if(!(validate160(data.source, {instancePath:instancePath+"/source",parentData:data,parentDataProperty:"source",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate160.errors : vErrors.concat(validate160.errors); errors = vErrors.length; } } @@ -22153,34 +24107,34 @@ vErrors.push(err46); } errors++; } -validate153.errors = vErrors; +validate159.errors = vErrors; return errors === 0; } -validate153.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate159.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -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"); +export const validateAsset = validate162; +const schema311 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://cloudhal.dev/schemas/v1/asset.schema.json","title":"Asset","description":"A media file of a site (image, video, audio, document, data) as a graph node: its bytes are identified by SHA-256 and stored once per site in the blob store under a content-addressed key, or kept elsewhere (`storage.kind: external`, e.g. a Zenodo or Hugging Face file). Pages, entries and terms refer to it by id (`ImageRef.assetId`, markdown `asset:`). Snapshotted like every node, so a deleted asset's bytes stay while any snapshot references it (RFC-0004).","type":"object","additionalProperties":false,"required":["schemaVersion","id","siteId","kind","mediaType","bytes","sha256","slug","storage","status","uploadedBy","source","rev","createdAt","updatedAt"],"properties":{"schemaVersion":{"$ref":"#/$defs/SchemaVersion"},"id":{"$ref":"#/$defs/Id"},"siteId":{"$ref":"#/$defs/Id"},"kind":{"type":"string","description":"What the file is, derived from the sniffed media type (never chosen by the client). `other` is allowed for external storage only.","enum":["image","video","audio","document","data","other"]},"mediaType":{"type":"string","description":"MIME type of the stored bytes as sniffed by the server (RFC 6838, lower case, no parameters), e.g. image/webp, text/csv. Served as Content-Type.","pattern":"^[a-z0-9][a-z0-9!#$&^_.+-]{0,126}/[a-z0-9][a-z0-9!#$&^_.+-]{0,126}$","maxLength":255},"bytes":{"type":"integer","description":"Size of the stored bytes. For external files: the size the source reports or the uploader declares.","minimum":0},"sha256":{"$ref":"#/$defs/Sha256","description":"SHA-256 of the stored bytes (lower-case hex): the blob key, the public URL prefix, the ETag and the checksum shown with downloads. Unique per site. For external files see `storage.checksum`."},"originalSha256":{"$ref":"#/$defs/Sha256","description":"SHA-256 of the bytes as uploaded, present only when the server changed them (image metadata stripping)."},"slug":{"$ref":"#/$defs/Slug","description":"File name in the public URL, without extension: /media//.. May change; the old URL then answers 301 to the new one."},"storage":{"$ref":"#/$defs/AssetStorage"},"image":{"$ref":"#/$defs/AssetImage"},"title":{"type":"object","description":"Display title per locale (download lists, library search).","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"type":"string","minLength":1,"maxLength":300}},"alt":{"type":"object","description":"Alternative text per locale. An `ImageRef.alt` that is not empty overrides it for one use.","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"type":"string","minLength":1,"maxLength":500}},"decorative":{"type":"boolean","description":"The image carries no information and renders with alt=\"\". Absent means false; the store never writes false."},"caption":{"type":"object","description":"Caption per locale (plain text), the default for figures that show this asset.","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"type":"string","minLength":1,"maxLength":1000}},"credit":{"type":"string","description":"Who made it, as shown next to it (e.g. 'Photo: Ayşe Yılmaz').","minLength":1,"maxLength":300},"license":{"$ref":"#/$defs/AssetLicense"},"status":{"type":"string","description":"active: listed and pickable. retired: hidden from the library and pickers and cannot be newly referenced; existing references keep rendering.","enum":["active","retired"]},"uploadedBy":{"$ref":"#/$defs/Actor"},"source":{"$ref":"#/$defs/AssetSource"},"rev":{"$ref":"#/$defs/Revision"},"createdAt":{"$ref":"#/$defs/Timestamp"},"updatedAt":{"$ref":"#/$defs/Timestamp"},"metadata":{"$ref":"#/$defs/Metadata"}},"$defs":{"Actor":{"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}},"ActorKind":{"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]},"AssetImage":{"type":"object","description":"Intrinsic size and placeholders of an image. Present for every image stored as a blob.","additionalProperties":false,"required":["width","height"],"properties":{"width":{"type":"integer","minimum":1,"maximum":100000},"height":{"type":"integer","minimum":1,"maximum":100000},"animated":{"type":"boolean","description":"Animated GIF or WebP: never resized or re-encoded. Absent means false; the store never writes false."},"blurhash":{"type":"string","description":"BlurHash of the image for themes that decode it client-side.","pattern":"^[0-9A-Za-z#$%*+,.:;=?@^_{|}~\\[\\]-]{6,100}$"},"lqip":{"type":"string","description":"Low-quality placeholder: a raster data: URL of at most about 3 KB, shown while the image loads.","pattern":"^data:image/(?:webp|png|jpeg);base64,[A-Za-z0-9+/]+={0,2}$","maxLength":4096},"color":{"type":"string","description":"RFC-0028: the image's dominant colour, lower-case #rrggbb. Painted under the placeholder and in color-fill bars.","pattern":"^#[0-9a-f]{6}$"},"alpha":{"type":"boolean","enum":[true],"description":"RFC-0028: the image may have transparent pixels (read from its header at ingest). Absent means opaque; transparent images never show the LQIP."},"focal":{"type":"object","description":"RFC-0028: where the subject is, from the top-left corner as fractions of the width and height. Cover crops keep it in view.","additionalProperties":false,"required":["x","y"],"properties":{"x":{"type":"number","minimum":0,"maximum":1},"y":{"type":"number","minimum":0,"maximum":1}}}}},"AssetLicense":{"description":"Terms the asset may be reused under: an SPDX license expression or a license URL.","oneOf":[{"type":"object","title":"SpdxLicense","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"type":"string","enum":["spdx"]},"id":{"type":"string","description":"SPDX license identifier or simple expression, e.g. CC-BY-4.0, CC0-1.0, MIT OR Apache-2.0, LicenseRef-AllRightsReserved.","pattern":"^[A-Za-z0-9][A-Za-z0-9.+-]*(?: (?:AND|OR|WITH) [A-Za-z0-9][A-Za-z0-9.+-]*)*$","maxLength":200}}},{"type":"object","title":"UrlLicense","additionalProperties":false,"required":["kind","url"],"properties":{"kind":{"type":"string","enum":["url"]},"url":{"$ref":"#/$defs/HttpsUrl"},"name":{"type":"string","minLength":1,"maxLength":200}}}]},"AssetSource":{"description":"How the asset came in. Immutable after create.","oneOf":[{"type":"object","title":"UploadSource","description":"Bytes uploaded by a person (REST) or an agent (MCP upload_asset).","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["upload"]},"filename":{"type":"string","description":"Original file name as sent (never used as a storage key or served).","pattern":"^[^/\\\\\\u0000-\\u001f\\u007f]+$","minLength":1,"maxLength":255}}},{"type":"object","title":"FetchSource","description":"Bytes the server fetched from an https URL (MCP fetch_asset_url, Studio 'From URL') under the SSRF rules.","additionalProperties":false,"required":["kind","url","fetchedAt"],"properties":{"kind":{"type":"string","enum":["fetch"]},"url":{"$ref":"#/$defs/HttpsUrl"},"fetchedAt":{"$ref":"#/$defs/Timestamp"}}},{"type":"object","title":"ImportSource","description":"Created by a migration or importer (e.g. `cloudhal media import-urls` from an ImageRef.src).","additionalProperties":false,"required":["kind","from"],"properties":{"kind":{"type":"string","enum":["import"]},"from":{"type":"string","description":"The reference it was imported from: a URL or a site path.","minLength":1,"maxLength":2048},"importer":{"$ref":"#/$defs/Slug"}}},{"type":"object","title":"LinkSource","description":"An external file registered by URL without copying its bytes (storage.kind external).","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["link"]}}}]},"AssetStorage":{"description":"Where the bytes live. `blob`: in this deployment's blob store (R2 or the filesystem) under the content-addressed key media//, derived and never stored, so the node is portable between backends. `external`: elsewhere, linked rather than hosted (large research files on Zenodo or Hugging Face).","oneOf":[{"type":"object","title":"BlobStorage","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["blob"]}}},{"type":"object","title":"ExternalStorage","additionalProperties":false,"required":["kind","url","checksum"],"properties":{"kind":{"type":"string","enum":["external"]},"url":{"$ref":"#/$defs/HttpsUrl","description":"Direct download URL of the file."},"landingPage":{"$ref":"#/$defs/HttpsUrl","description":"Human page of the record the file belongs to (e.g. a Zenodo record or a Hugging Face dataset)."},"checksum":{"type":"string","description":"How `sha256` was obtained. computed: the server or `cloudhal media verify` streamed the URL and hashed it. declared: given by the person who linked it, not verified (shown as such).","enum":["computed","declared"]},"verifiedAt":{"$ref":"#/$defs/Timestamp","description":"When the URL last produced bytes with this SHA-256 (checksum `computed`)."}}}]},"HttpsUrl":{"type":"string","description":"Absolute https URL without credentials, whitespace or backslashes.","pattern":"^https://[^\\s\\\\/?#@]+(?:[/?#][^\\s\\\\]*)?$","maxLength":2048},"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"Locale":{"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"},"Metadata":{"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}},"Revision":{"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0},"SchemaVersion":{"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]},"Sha256":{"type":"string","description":"Lower-case hex SHA-256 digest.","pattern":"^[0-9a-f]{64}$"},"Slug":{"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}}}; +const schema312 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; +const schema313 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; +const schema315 = {"type":"string","description":"Lower-case hex SHA-256 digest.","pattern":"^[0-9a-f]{64}$"}; +const schema317 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; +const schema322 = {"type":"object","description":"Intrinsic size and placeholders of an image. Present for every image stored as a blob.","additionalProperties":false,"required":["width","height"],"properties":{"width":{"type":"integer","minimum":1,"maximum":100000},"height":{"type":"integer","minimum":1,"maximum":100000},"animated":{"type":"boolean","description":"Animated GIF or WebP: never resized or re-encoded. Absent means false; the store never writes false."},"blurhash":{"type":"string","description":"BlurHash of the image for themes that decode it client-side.","pattern":"^[0-9A-Za-z#$%*+,.:;=?@^_{|}~\\[\\]-]{6,100}$"},"lqip":{"type":"string","description":"Low-quality placeholder: a raster data: URL of at most about 3 KB, shown while the image loads.","pattern":"^data:image/(?:webp|png|jpeg);base64,[A-Za-z0-9+/]+={0,2}$","maxLength":4096},"color":{"type":"string","description":"RFC-0028: the image's dominant colour, lower-case #rrggbb. Painted under the placeholder and in color-fill bars.","pattern":"^#[0-9a-f]{6}$"},"alpha":{"type":"boolean","enum":[true],"description":"RFC-0028: the image may have transparent pixels (read from its header at ingest). Absent means opaque; transparent images never show the LQIP."},"focal":{"type":"object","description":"RFC-0028: where the subject is, from the top-left corner as fractions of the width and height. Cover crops keep it in view.","additionalProperties":false,"required":["x","y"],"properties":{"x":{"type":"number","minimum":0,"maximum":1},"y":{"type":"number","minimum":0,"maximum":1}}}}}; +const schema323 = {"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"}; +const schema335 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; +const schema321 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema338 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; +const pattern165 = new RegExp("^[a-z0-9][a-z0-9!#$&^_.+-]{0,126}/[a-z0-9][a-z0-9!#$&^_.+-]{0,126}$", "u"); +const pattern171 = new RegExp("^[0-9A-Za-z#$%*+,.:;=?@^_{|}~\\[\\]-]{6,100}$", "u"); +const pattern172 = new RegExp("^data:image/(?:webp|png|jpeg);base64,[A-Za-z0-9+/]+={0,2}$", "u"); +const pattern173 = new RegExp("^#[0-9a-f]{6}$", "u"); +const schema318 = {"description":"Where the bytes live. `blob`: in this deployment's blob store (R2 or the filesystem) under the content-addressed key media//, derived and never stored, so the node is portable between backends. `external`: elsewhere, linked rather than hosted (large research files on Zenodo or Hugging Face).","oneOf":[{"type":"object","title":"BlobStorage","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["blob"]}}},{"type":"object","title":"ExternalStorage","additionalProperties":false,"required":["kind","url","checksum"],"properties":{"kind":{"type":"string","enum":["external"]},"url":{"$ref":"#/$defs/HttpsUrl","description":"Direct download URL of the file."},"landingPage":{"$ref":"#/$defs/HttpsUrl","description":"Human page of the record the file belongs to (e.g. a Zenodo record or a Hugging Face dataset)."},"checksum":{"type":"string","description":"How `sha256` was obtained. computed: the server or `cloudhal media verify` streamed the URL and hashed it. declared: given by the person who linked it, not verified (shown as such).","enum":["computed","declared"]},"verifiedAt":{"$ref":"#/$defs/Timestamp","description":"When the URL last produced bytes with this SHA-256 (checksum `computed`)."}}}]}; +const schema319 = {"type":"string","description":"Absolute https URL without credentials, whitespace or backslashes.","pattern":"^https://[^\\s\\\\/?#@]+(?:[/?#][^\\s\\\\]*)?$","maxLength":2048}; +const pattern169 = new RegExp("^https://[^\\s\\\\/?#@]+(?:[/?#][^\\s\\\\]*)?$", "u"); -function validate157(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate163(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate157.evaluated; +const evaluated0 = validate163.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -22227,7 +24181,7 @@ vErrors.push(err2); errors++; } if(!(data0 === "blob")){ -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"}; +const err3 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema318.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err3]; } @@ -22311,7 +24265,7 @@ vErrors.push(err9); errors++; } if(!(data1 === "external")){ -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"}; +const err10 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema318.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err10]; } @@ -22334,7 +24288,7 @@ vErrors.push(err11); } errors++; } -if(!pattern158.test(data2)){ +if(!pattern169.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]; @@ -22369,7 +24323,7 @@ vErrors.push(err14); } errors++; } -if(!pattern158.test(data3)){ +if(!pattern169.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]; @@ -22404,7 +24358,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: schema304.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: schema318.oneOf[1].properties.checksum.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err18]; } @@ -22485,19 +24439,19 @@ vErrors = null; } } } -validate157.errors = vErrors; +validate163.errors = vErrors; evaluated0.props = props0; return errors === 0; } -validate157.evaluated = {"dynamicProps":true,"dynamicItems":false}; +validate163.evaluated = {"dynamicProps":true,"dynamicItems":false}; -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"); +const schema326 = {"description":"Terms the asset may be reused under: an SPDX license expression or a license URL.","oneOf":[{"type":"object","title":"SpdxLicense","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"type":"string","enum":["spdx"]},"id":{"type":"string","description":"SPDX license identifier or simple expression, e.g. CC-BY-4.0, CC0-1.0, MIT OR Apache-2.0, LicenseRef-AllRightsReserved.","pattern":"^[A-Za-z0-9][A-Za-z0-9.+-]*(?: (?:AND|OR|WITH) [A-Za-z0-9][A-Za-z0-9.+-]*)*$","maxLength":200}}},{"type":"object","title":"UrlLicense","additionalProperties":false,"required":["kind","url"],"properties":{"kind":{"type":"string","enum":["url"]},"url":{"$ref":"#/$defs/HttpsUrl"},"name":{"type":"string","minLength":1,"maxLength":200}}}]}; +const pattern177 = new RegExp("^[A-Za-z0-9][A-Za-z0-9.+-]*(?: (?:AND|OR|WITH) [A-Za-z0-9][A-Za-z0-9.+-]*)*$", "u"); -function validate159(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate165(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate159.evaluated; +const evaluated0 = validate165.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -22554,7 +24508,7 @@ vErrors.push(err3); errors++; } if(!(data0 === "spdx")){ -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"}; +const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema326.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -22577,7 +24531,7 @@ vErrors.push(err5); } errors++; } -if(!pattern166.test(data1)){ +if(!pattern177.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]; @@ -22663,7 +24617,7 @@ vErrors.push(err12); errors++; } if(!(data2 === "url")){ -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"}; +const err13 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema326.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err13]; } @@ -22686,7 +24640,7 @@ vErrors.push(err14); } errors++; } -if(!pattern158.test(data3)){ +if(!pattern169.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]; @@ -22789,19 +24743,19 @@ vErrors = null; } } } -validate159.errors = vErrors; +validate165.errors = vErrors; evaluated0.props = props0; return errors === 0; } -validate159.evaluated = {"dynamicProps":true,"dynamicItems":false}; +validate165.evaluated = {"dynamicProps":true,"dynamicItems":false}; -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"]}; +const schema328 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; +const schema329 = {"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]}; -function validate161(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate167(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate161.evaluated; +const evaluated0 = validate167.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -22854,7 +24808,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: schema315.enum},message:"must be equal to one of the allowed values"}; +const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema329.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -22945,18 +24899,18 @@ vErrors.push(err11); } errors++; } -validate161.errors = vErrors; +validate167.errors = vErrors; return errors === 0; } -validate161.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate167.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -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"); +const schema331 = {"description":"How the asset came in. Immutable after create.","oneOf":[{"type":"object","title":"UploadSource","description":"Bytes uploaded by a person (REST) or an agent (MCP upload_asset).","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["upload"]},"filename":{"type":"string","description":"Original file name as sent (never used as a storage key or served).","pattern":"^[^/\\\\\\u0000-\\u001f\\u007f]+$","minLength":1,"maxLength":255}}},{"type":"object","title":"FetchSource","description":"Bytes the server fetched from an https URL (MCP fetch_asset_url, Studio 'From URL') under the SSRF rules.","additionalProperties":false,"required":["kind","url","fetchedAt"],"properties":{"kind":{"type":"string","enum":["fetch"]},"url":{"$ref":"#/$defs/HttpsUrl"},"fetchedAt":{"$ref":"#/$defs/Timestamp"}}},{"type":"object","title":"ImportSource","description":"Created by a migration or importer (e.g. `cloudhal media import-urls` from an ImageRef.src).","additionalProperties":false,"required":["kind","from"],"properties":{"kind":{"type":"string","enum":["import"]},"from":{"type":"string","description":"The reference it was imported from: a URL or a site path.","minLength":1,"maxLength":2048},"importer":{"$ref":"#/$defs/Slug"}}},{"type":"object","title":"LinkSource","description":"An external file registered by URL without copying its bytes (storage.kind external).","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["link"]}}}]}; +const pattern180 = new RegExp("^[^/\\\\\\u0000-\\u001f\\u007f]+$", "u"); -function validate163(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate169(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate163.evaluated; +const evaluated0 = validate169.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -23003,7 +24957,7 @@ vErrors.push(err2); errors++; } if(!(data0 === "upload")){ -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"}; +const err3 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema331.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err3]; } @@ -23036,7 +24990,7 @@ vErrors.push(err5); } errors++; } -if(!pattern169.test(data1)){ +if(!pattern180.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]; @@ -23132,7 +25086,7 @@ vErrors.push(err13); errors++; } if(!(data2 === "fetch")){ -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"}; +const err14 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema331.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err14]; } @@ -23155,7 +25109,7 @@ vErrors.push(err15); } errors++; } -if(!pattern158.test(data3)){ +if(!pattern169.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]; @@ -23273,7 +25227,7 @@ vErrors.push(err24); errors++; } if(!(data5 === "import")){ -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"}; +const err25 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/2/properties/kind/enum",keyword:"enum",params:{allowedValues: schema331.oneOf[2].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err25]; } @@ -23424,7 +25378,7 @@ vErrors.push(err36); errors++; } if(!(data8 === "link")){ -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"}; +const err37 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/3/properties/kind/enum",keyword:"enum",params:{allowedValues: schema331.oneOf[3].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err37]; } @@ -23482,18 +25436,18 @@ vErrors = null; } } } -validate163.errors = vErrors; +validate169.errors = vErrors; evaluated0.props = props0; return errors === 0; } -validate163.evaluated = {"dynamicProps":true,"dynamicItems":false}; +validate169.evaluated = {"dynamicProps":true,"dynamicItems":false}; -function validate156(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate162(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ /*# sourceURL="https://cloudhal.dev/schemas/v1/asset.schema.json" */; let vErrors = null; let errors = 0; -const evaluated0 = validate156.evaluated; +const evaluated0 = validate162.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -23652,7 +25606,7 @@ vErrors.push(err14); errors++; } for(const key0 in data){ -if(!(func1.call(schema297.properties, key0))){ +if(!(func1.call(schema311.properties, key0))){ const err15 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}; if(vErrors === null){ vErrors = [err15]; @@ -23676,7 +25630,7 @@ vErrors.push(err16); errors++; } if(!(data0 === 1)){ -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"}; +const err17 = {instancePath:instancePath+"/schemaVersion",schemaPath:"#/$defs/SchemaVersion/enum",keyword:"enum",params:{allowedValues: schema312.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err17]; } @@ -23789,7 +25743,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: schema297.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: schema311.properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err27]; } @@ -23812,7 +25766,7 @@ vErrors.push(err28); } errors++; } -if(!pattern154.test(data4)){ +if(!pattern165.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]; @@ -23862,7 +25816,7 @@ errors++; if(data.sha256 !== undefined){ let data6 = data.sha256; if(typeof data6 === "string"){ -if(!pattern45.test(data6)){ +if(!pattern50.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]; @@ -23887,7 +25841,7 @@ errors++; if(data.originalSha256 !== undefined){ let data7 = data.originalSha256; if(typeof data7 === "string"){ -if(!pattern45.test(data7)){ +if(!pattern50.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]; @@ -23955,8 +25909,8 @@ errors++; } } if(data.storage !== undefined){ -if(!(validate157(data.storage, {instancePath:instancePath+"/storage",parentData:data,parentDataProperty:"storage",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate157.errors : vErrors.concat(validate157.errors); +if(!(validate163(data.storage, {instancePath:instancePath+"/storage",parentData:data,parentDataProperty:"storage",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate163.errors : vErrors.concat(validate163.errors); errors = vErrors.length; } } @@ -24080,7 +26034,7 @@ errors++; if(data10.blurhash !== undefined){ let data14 = data10.blurhash; if(typeof data14 === "string"){ -if(!pattern160.test(data14)){ +if(!pattern171.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]; @@ -24115,7 +26069,7 @@ vErrors.push(err53); } errors++; } -if(!pattern161.test(data15)){ +if(!pattern172.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]; @@ -24140,7 +26094,7 @@ errors++; if(data10.color !== undefined){ let data16 = data10.color; if(typeof data16 === "string"){ -if(!pattern162.test(data16)){ +if(!pattern173.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]; @@ -24175,7 +26129,7 @@ vErrors.push(err58); errors++; } if(!(data17 === true)){ -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"}; +const err59 = {instancePath:instancePath+"/image/alpha",schemaPath:"#/$defs/AssetImage/properties/alpha/enum",keyword:"enum",params:{allowedValues: schema322.properties.alpha.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err59]; } @@ -24650,8 +26604,8 @@ errors++; } } if(data.license !== undefined){ -if(!(validate159(data.license, {instancePath:instancePath+"/license",parentData:data,parentDataProperty:"license",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate159.errors : vErrors.concat(validate159.errors); +if(!(validate165(data.license, {instancePath:instancePath+"/license",parentData:data,parentDataProperty:"license",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate165.errors : vErrors.concat(validate165.errors); errors = vErrors.length; } } @@ -24668,7 +26622,7 @@ vErrors.push(err99); errors++; } if(!((data30 === "active") || (data30 === "retired"))){ -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"}; +const err100 = {instancePath:instancePath+"/status",schemaPath:"#/properties/status/enum",keyword:"enum",params:{allowedValues: schema311.properties.status.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err100]; } @@ -24679,14 +26633,14 @@ errors++; } } if(data.uploadedBy !== undefined){ -if(!(validate161(data.uploadedBy, {instancePath:instancePath+"/uploadedBy",parentData:data,parentDataProperty:"uploadedBy",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate161.errors : vErrors.concat(validate161.errors); +if(!(validate167(data.uploadedBy, {instancePath:instancePath+"/uploadedBy",parentData:data,parentDataProperty:"uploadedBy",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate167.errors : vErrors.concat(validate167.errors); errors = vErrors.length; } } if(data.source !== undefined){ -if(!(validate163(data.source, {instancePath:instancePath+"/source",parentData:data,parentDataProperty:"source",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate163.errors : vErrors.concat(validate163.errors); +if(!(validate169(data.source, {instancePath:instancePath+"/source",parentData:data,parentDataProperty:"source",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate169.errors : vErrors.concat(validate169.errors); errors = vErrors.length; } } @@ -24803,30 +26757,30 @@ vErrors.push(err109); } errors++; } -validate156.errors = vErrors; +validate162.errors = vErrors; return errors === 0; } -validate156.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate162.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -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"); +export const validateMenu = validate171; +const schema339 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://cloudhal.dev/schemas/v1/menu.schema.json","title":"Menu","description":"A named navigation menu of a site (e.g. 'primary', 'footer', 'sidebar'): an ordered tree of items. Items point at pages, entries and terms by id, so renames and slug changes never break them; an item whose target is gone, retired or unpublished is hidden by the renderer, never rendered as a dead link (RFC-0003).","type":"object","additionalProperties":false,"required":["schemaVersion","id","siteId","key","maxDepth","items","rev","createdAt","updatedAt"],"properties":{"schemaVersion":{"$ref":"#/$defs/SchemaVersion"},"id":{"$ref":"#/$defs/Id"},"siteId":{"$ref":"#/$defs/Id"},"key":{"$ref":"#/$defs/Slug","description":"Where themes place the menu ('primary', 'footer', 'sidebar', …). Unique per site and immutable once created."},"maxDepth":{"type":"integer","description":"Deepest level an item may sit at; top-level items are level 1. The store refuses deeper trees.","minimum":1,"maximum":6},"items":{"type":"array","description":"Top-level items in display order. Empty means the site deliberately shows no items here (a missing menu means the theme's derived default).","items":{"$ref":"#/$defs/MenuItem"},"maxItems":100},"rev":{"$ref":"#/$defs/Revision"},"createdAt":{"$ref":"#/$defs/Timestamp"},"updatedAt":{"$ref":"#/$defs/Timestamp"},"metadata":{"$ref":"#/$defs/Metadata"}},"$defs":{"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"Locale":{"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"},"MenuDisplay":{"type":"string","description":"RFC-0025 25d (RFC-0003 amendment): how a top-level item with children opens in the header. `mega`: one wide panel, its children as columns (a group's children are the column's links).","enum":["mega"]},"MenuItem":{"description":"One item, discriminated by `kind` (what it links to). Children, in display order, form the tree; an item has exactly one parent by construction, so the tree has no cycles.","oneOf":[{"type":"object","title":"PageMenuItem","description":"Links to a page by id; the label falls back to the page title.","additionalProperties":false,"required":["id","kind","pageId"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["page"]},"pageId":{"$ref":"#/$defs/Id"},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"EntryMenuItem","description":"Links to a content entry by id (at //); the label falls back to the entry title.","additionalProperties":false,"required":["id","kind","entryId"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["contentEntry"]},"entryId":{"$ref":"#/$defs/Id"},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"TermMenuItem","description":"Links to a term's archive by term id; the label falls back to the term name in the locale.","additionalProperties":false,"required":["id","kind","termId"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["term"]},"termId":{"$ref":"#/$defs/Id"},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"CollectionMenuItem","description":"Links to the index of a content collection, wherever the theme serves it. Collections have no node and no title, so labels are required.","additionalProperties":false,"required":["id","kind","collection","labels"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["collection"]},"collection":{"$ref":"#/$defs/Slug"},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"UrlMenuItem","description":"Links to an external https URL. The host is ASCII (internationalised names in punycode) and carries no user-info, so a label cannot hide where the link goes.","additionalProperties":false,"required":["id","kind","href","labels"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["url"]},"href":{"type":"string","pattern":"^https://[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?(?::[0-9]{1,5})?(?:[/?#][^\\s\\\\\\x00-\\x1F\\x7F]*)?$","maxLength":2048},"newTab":{"type":"boolean","description":"Open in a new tab (rendered with rel=\"noopener noreferrer\")."},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"GroupMenuItem","description":"A label without a link that holds other items: a dropdown parent or a sidebar section heading. Hidden when none of its children is shown.","additionalProperties":false,"required":["id","kind","labels","children"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["group"]},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}}]},"MenuLabels":{"type":"object","description":"Item label per locale; plain text, no control characters or surrounding whitespace. A locale without a label falls back to the default locale's label, then to the target's own title or name.","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[^\\s\\x00-\\x1F\\x7F](?:[^\\x00-\\x1F\\x7F]*[^\\s\\x00-\\x1F\\x7F])?$"}},"Metadata":{"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}},"Revision":{"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0},"SchemaVersion":{"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]},"Slug":{"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}}}; +const schema340 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; +const schema341 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; +const schema343 = {"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128}; +const schema363 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; +const schema364 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema366 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; +const schema344 = {"description":"One item, discriminated by `kind` (what it links to). Children, in display order, form the tree; an item has exactly one parent by construction, so the tree has no cycles.","oneOf":[{"type":"object","title":"PageMenuItem","description":"Links to a page by id; the label falls back to the page title.","additionalProperties":false,"required":["id","kind","pageId"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["page"]},"pageId":{"$ref":"#/$defs/Id"},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"EntryMenuItem","description":"Links to a content entry by id (at //); the label falls back to the entry title.","additionalProperties":false,"required":["id","kind","entryId"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["contentEntry"]},"entryId":{"$ref":"#/$defs/Id"},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"TermMenuItem","description":"Links to a term's archive by term id; the label falls back to the term name in the locale.","additionalProperties":false,"required":["id","kind","termId"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["term"]},"termId":{"$ref":"#/$defs/Id"},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"CollectionMenuItem","description":"Links to the index of a content collection, wherever the theme serves it. Collections have no node and no title, so labels are required.","additionalProperties":false,"required":["id","kind","collection","labels"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["collection"]},"collection":{"$ref":"#/$defs/Slug"},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"UrlMenuItem","description":"Links to an external https URL. The host is ASCII (internationalised names in punycode) and carries no user-info, so a label cannot hide where the link goes.","additionalProperties":false,"required":["id","kind","href","labels"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["url"]},"href":{"type":"string","pattern":"^https://[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?(?::[0-9]{1,5})?(?:[/?#][^\\s\\\\\\x00-\\x1F\\x7F]*)?$","maxLength":2048},"newTab":{"type":"boolean","description":"Open in a new tab (rendered with rel=\"noopener noreferrer\")."},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}},{"type":"object","title":"GroupMenuItem","description":"A label without a link that holds other items: a dropdown parent or a sidebar section heading. Hidden when none of its children is shown.","additionalProperties":false,"required":["id","kind","labels","children"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["group"]},"labels":{"$ref":"#/$defs/MenuLabels"},"display":{"$ref":"#/$defs/MenuDisplay"},"children":{"type":"array","items":{"$ref":"#/$defs/MenuItem"},"minItems":1,"maxItems":100}}}]}; +const schema349 = {"type":"string","description":"RFC-0025 25d (RFC-0003 amendment): how a top-level item with children opens in the header. `mega`: one wide panel, its children as columns (a group's children are the column's links).","enum":["mega"]}; +const pattern197 = new RegExp("^https://[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?(?::[0-9]{1,5})?(?:[/?#][^\\s\\\\\\x00-\\x1F\\x7F]*)?$", "u"); +const schema347 = {"type":"object","description":"Item label per locale; plain text, no control characters or surrounding whitespace. A locale without a label falls back to the default locale's label, then to the target's own title or name.","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[^\\s\\x00-\\x1F\\x7F](?:[^\\x00-\\x1F\\x7F]*[^\\s\\x00-\\x1F\\x7F])?$"}}; +const schema348 = {"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"}; +const pattern189 = new RegExp("^[^\\s\\x00-\\x1F\\x7F](?:[^\\x00-\\x1F\\x7F]*[^\\s\\x00-\\x1F\\x7F])?$", "u"); -function validate167(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate173(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate167.evaluated; +const evaluated0 = validate173.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -24903,7 +26857,7 @@ vErrors.push(err5); } errors++; } -if(!pattern178.test(data0)){ +if(!pattern189.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]; @@ -24936,17 +26890,17 @@ vErrors.push(err8); } errors++; } -validate167.errors = vErrors; +validate173.errors = vErrors; return errors === 0; } -validate167.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate173.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -const wrapper0 = {validate: validate166}; +const wrapper0 = {validate: validate172}; -function validate166(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate172(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate166.evaluated; +const evaluated0 = validate172.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -25058,7 +27012,7 @@ vErrors.push(err8); errors++; } if(!(data1 === "page")){ -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"}; +const err9 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema344.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err9]; } @@ -25114,8 +27068,8 @@ errors++; } } if(data.labels !== undefined){ -if(!(validate167(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate167.errors : vErrors.concat(validate167.errors); +if(!(validate173(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate173.errors : vErrors.concat(validate173.errors); errors = vErrors.length; } } @@ -25132,7 +27086,7 @@ vErrors.push(err14); errors++; } if(!(data4 === "mega")){ -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"}; +const err15 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema349.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err15]; } @@ -25303,7 +27257,7 @@ vErrors.push(err28); errors++; } if(!(data8 === "contentEntry")){ -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"}; +const err29 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema344.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err29]; } @@ -25359,8 +27313,8 @@ errors++; } } if(data.labels !== undefined){ -if(!(validate167(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate167.errors : vErrors.concat(validate167.errors); +if(!(validate173(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate173.errors : vErrors.concat(validate173.errors); errors = vErrors.length; } } @@ -25377,7 +27331,7 @@ vErrors.push(err34); errors++; } if(!(data11 === "mega")){ -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"}; +const err35 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema349.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err35]; } @@ -25555,7 +27509,7 @@ vErrors.push(err48); errors++; } if(!(data15 === "term")){ -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"}; +const err49 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/2/properties/kind/enum",keyword:"enum",params:{allowedValues: schema344.oneOf[2].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err49]; } @@ -25611,8 +27565,8 @@ errors++; } } if(data.labels !== undefined){ -if(!(validate167(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate167.errors : vErrors.concat(validate167.errors); +if(!(validate173(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate173.errors : vErrors.concat(validate173.errors); errors = vErrors.length; } } @@ -25629,7 +27583,7 @@ vErrors.push(err54); errors++; } if(!(data18 === "mega")){ -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"}; +const err55 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema349.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err55]; } @@ -25817,7 +27771,7 @@ vErrors.push(err69); errors++; } if(!(data22 === "collection")){ -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"}; +const err70 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/3/properties/kind/enum",keyword:"enum",params:{allowedValues: schema344.oneOf[3].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err70]; } @@ -25873,8 +27827,8 @@ errors++; } } if(data.labels !== undefined){ -if(!(validate167(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate167.errors : vErrors.concat(validate167.errors); +if(!(validate173(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate173.errors : vErrors.concat(validate173.errors); errors = vErrors.length; } } @@ -25891,7 +27845,7 @@ vErrors.push(err75); errors++; } if(!(data25 === "mega")){ -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"}; +const err76 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema349.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err76]; } @@ -26079,7 +28033,7 @@ vErrors.push(err90); errors++; } if(!(data29 === "url")){ -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"}; +const err91 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/4/properties/kind/enum",keyword:"enum",params:{allowedValues: schema344.oneOf[4].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err91]; } @@ -26102,7 +28056,7 @@ vErrors.push(err92); } errors++; } -if(!pattern186.test(data30)){ +if(!pattern197.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]; @@ -26137,8 +28091,8 @@ errors++; } } if(data.labels !== undefined){ -if(!(validate167(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate167.errors : vErrors.concat(validate167.errors); +if(!(validate173(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate173.errors : vErrors.concat(validate173.errors); errors = vErrors.length; } } @@ -26155,7 +28109,7 @@ vErrors.push(err96); errors++; } if(!(data33 === "mega")){ -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"}; +const err97 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema349.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err97]; } @@ -26343,7 +28297,7 @@ vErrors.push(err111); errors++; } if(!(data37 === "group")){ -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"}; +const err112 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/5/properties/kind/enum",keyword:"enum",params:{allowedValues: schema344.oneOf[5].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err112]; } @@ -26354,8 +28308,8 @@ errors++; } } if(data.labels !== undefined){ -if(!(validate167(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate167.errors : vErrors.concat(validate167.errors); +if(!(validate173(data.labels, {instancePath:instancePath+"/labels",parentData:data,parentDataProperty:"labels",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate173.errors : vErrors.concat(validate173.errors); errors = vErrors.length; } } @@ -26372,7 +28326,7 @@ vErrors.push(err113); errors++; } if(!(data39 === "mega")){ -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"}; +const err114 = {instancePath:instancePath+"/display",schemaPath:"#/$defs/MenuDisplay/enum",keyword:"enum",params:{allowedValues: schema349.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err114]; } @@ -26474,18 +28428,18 @@ vErrors = null; } } } -validate166.errors = vErrors; +validate172.errors = vErrors; evaluated0.props = props1; return errors === 0; } -validate166.evaluated = {"dynamicProps":true,"dynamicItems":false}; +validate172.evaluated = {"dynamicProps":true,"dynamicItems":false}; -function validate165(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate171(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ /*# sourceURL="https://cloudhal.dev/schemas/v1/menu.schema.json" */; let vErrors = null; let errors = 0; -const evaluated0 = validate165.evaluated; +const evaluated0 = validate171.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -26584,7 +28538,7 @@ vErrors.push(err8); errors++; } for(const key0 in data){ -if(!(func1.call(schema325.properties, key0))){ +if(!(func1.call(schema339.properties, key0))){ const err9 = {instancePath,schemaPath:"#/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key0},message:"must NOT have additional properties"}; if(vErrors === null){ vErrors = [err9]; @@ -26608,7 +28562,7 @@ vErrors.push(err10); errors++; } if(!(data0 === 1)){ -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"}; +const err11 = {instancePath:instancePath+"/schemaVersion",schemaPath:"#/$defs/SchemaVersion/enum",keyword:"enum",params:{allowedValues: schema340.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err11]; } @@ -26803,8 +28757,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 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}$"}}}; +export const validateRelease = validate195; +const schema464 = {"$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 schema465 = {"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]}; +const schema466 = {"type":"string","description":"A release id: `rel_` and 6 to 32 letters or digits.","pattern":"^rel_[A-Za-z0-9]{6,32}$"}; +const schema467 = {"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48}; +const schema479 = {"type":"string","description":"Lifecycle. `draft`: proposed or being assembled, nothing locked. `awaiting_approval`: waiting for a person; its nodes are locked. `scheduled`: approved (or allowed by `auto` mode) and locked until it runs. `running`: claimed by the scheduler. `published`: applied; `outcome` names the snapshot. `conflict`: a pinned node or its live base changed; held, nothing published. `missed`: the scheduler reached it more than an hour late; nothing published. `expired`: no valid approval at its time. `failed`: refused at run time (e.g. a dependency is missing). `canceled`: withdrawn.","enum":["draft","awaiting_approval","scheduled","running","published","conflict","missed","expired","failed","canceled"]}; +const schema480 = {"type":"string","description":"`rl1:` and the lower-case hex SHA-256 of the release's canonical item lines (site id, then one line per item sorted by node id: kind, node id, target and base versions). The prefix keeps it apart from the graph state hash and from comment bindings (`cm1:`); approvals store it as their state.","pattern":"^rl1:[0-9a-f]{64}$"}; +const schema476 = {"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"}; +const schema472 = {"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0}; +const schema508 = {"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}}; +const pattern246 = new RegExp("^rel_[A-Za-z0-9]{6,32}$", "u"); +const pattern248 = new RegExp("^[^\\x00-\\x1F\\x7F\\u202A-\\u202E\\u2066-\\u2069]+$", "u"); +const pattern254 = new RegExp("^rl1:[0-9a-f]{64}$", "u"); +const schema468 = {"description":"One pinned change, discriminated by `kind`.","oneOf":[{"type":"object","title":"ReleaseSetItem","description":"Make `target` the live version of the node. `base` is the version that was live when the item was pinned; absent when the node was not live (a new entry or page).","additionalProperties":false,"required":["kind","nodeId","nodeKind","target"],"properties":{"kind":{"type":"string","enum":["set"]},"nodeId":{"$ref":"#/$defs/Id"},"nodeKind":{"$ref":"#/$defs/ReleaseNodeKind"},"target":{"$ref":"#/$defs/ReleaseNodeVersion"},"base":{"$ref":"#/$defs/ReleaseNodeVersion"}}},{"type":"object","title":"ReleaseRemoveItem","description":"Take the node out of the live snapshot. `base` is the live version that is removed.","additionalProperties":false,"required":["kind","nodeId","nodeKind","base"],"properties":{"kind":{"type":"string","enum":["remove"]},"nodeId":{"$ref":"#/$defs/Id"},"nodeKind":{"$ref":"#/$defs/ReleaseNodeKind"},"base":{"$ref":"#/$defs/ReleaseNodeVersion"}}}]}; +const schema470 = {"type":"string","description":"Kinds of graph node a release may carry. The site node is never one of them.","enum":["page","contentEntry","taxonomy","term","redirect","menu","asset"]}; +const schema471 = {"type":"object","description":"One version of a node: its revision and the SHA-256 of its canonical JSON, whose body the blob store keeps under `nodes/.json`.","additionalProperties":false,"required":["rev","hash"],"properties":{"rev":{"$ref":"#/$defs/Revision"},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$"}}}; -function validate191(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate197(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate191.evaluated; +const evaluated0 = validate197.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -32492,7 +34446,7 @@ errors++; if(data.hash !== undefined){ let data1 = data.hash; if(typeof data1 === "string"){ -if(!pattern45.test(data1)){ +if(!pattern50.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]; @@ -32525,16 +34479,16 @@ vErrors.push(err7); } errors++; } -validate191.errors = vErrors; +validate197.errors = vErrors; return errors === 0; } -validate191.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate197.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate190(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate196(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate190.evaluated; +const evaluated0 = validate196.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -32611,7 +34565,7 @@ vErrors.push(err5); errors++; } if(!(data0 === "set")){ -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"}; +const err6 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema468.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err6]; } @@ -32679,7 +34633,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: schema456.enum},message:"must be equal to one of the allowed values"}; +const err12 = {instancePath:instancePath+"/nodeKind",schemaPath:"#/$defs/ReleaseNodeKind/enum",keyword:"enum",params:{allowedValues: schema470.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err12]; } @@ -32690,14 +34644,14 @@ errors++; } } if(data.target !== undefined){ -if(!(validate191(data.target, {instancePath:instancePath+"/target",parentData:data,parentDataProperty:"target",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate191.errors : vErrors.concat(validate191.errors); +if(!(validate197(data.target, {instancePath:instancePath+"/target",parentData:data,parentDataProperty:"target",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate197.errors : vErrors.concat(validate197.errors); errors = vErrors.length; } } if(data.base !== undefined){ -if(!(validate191(data.base, {instancePath:instancePath+"/base",parentData:data,parentDataProperty:"base",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate191.errors : vErrors.concat(validate191.errors); +if(!(validate197(data.base, {instancePath:instancePath+"/base",parentData:data,parentDataProperty:"base",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate197.errors : vErrors.concat(validate197.errors); errors = vErrors.length; } } @@ -32785,7 +34739,7 @@ vErrors.push(err19); errors++; } if(!(data5 === "remove")){ -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"}; +const err20 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema468.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err20]; } @@ -32853,7 +34807,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: schema456.enum},message:"must be equal to one of the allowed values"}; +const err26 = {instancePath:instancePath+"/nodeKind",schemaPath:"#/$defs/ReleaseNodeKind/enum",keyword:"enum",params:{allowedValues: schema470.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err26]; } @@ -32864,8 +34818,8 @@ errors++; } } if(data.base !== undefined){ -if(!(validate191(data.base, {instancePath:instancePath+"/base",parentData:data,parentDataProperty:"base",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate191.errors : vErrors.concat(validate191.errors); +if(!(validate197(data.base, {instancePath:instancePath+"/base",parentData:data,parentDataProperty:"base",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate197.errors : vErrors.concat(validate197.errors); errors = vErrors.length; } } @@ -32915,21 +34869,21 @@ vErrors = null; } } } -validate190.errors = vErrors; +validate196.errors = vErrors; evaluated0.props = props0; return errors === 0; } -validate190.evaluated = {"dynamicProps":true,"dynamicItems":false}; +validate196.evaluated = {"dynamicProps":true,"dynamicItems":false}; -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"); +const schema475 = {"description":"When the release runs, discriminated by `kind`. Absent while a draft has no time yet.","oneOf":[{"type":"object","title":"ReleaseAt","description":"At an instant. `at` is what the scheduler compares with the clock; when a person entered a wall-clock time, `localTime` keeps it and `at` is recomputed from it and `timeZone` before the release runs, never earlier than the wall-clock time.","additionalProperties":false,"required":["kind","at","timeZone"],"properties":{"kind":{"type":"string","enum":["at"]},"at":{"$ref":"#/$defs/Timestamp"},"timeZone":{"$ref":"#/$defs/TimeZone"},"localTime":{"$ref":"#/$defs/LocalDateTime"}}},{"type":"object","title":"ReleaseOnApproval","description":"As soon as it is approved (a gate-only change such as setting an expiry on live content, or \"publish when approved\").","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["on-approval"]}}}]}; +const schema477 = {"type":"string","description":"IANA time zone name from the tz database (RFC-0008), e.g. Europe/Istanbul, America/Argentina/Buenos_Aires, Etc/GMT+3 or UTC. Wall-clock times a person enters are kept with their zone, so the instant is recomputed when governments change the rules. The pattern bounds the shape only; writers also check the name against the runtime's Intl zone list.","pattern":"^(?:UTC|[A-Z][A-Za-z_-]+(?:/[A-Za-z0-9_+-]+){1,2})$","maxLength":64}; +const schema478 = {"type":"string","description":"A wall-clock date and time without an offset, to the minute (YYYY-MM-DDTHH:mm), as a person entered it; always paired with a TimeZone (RFC-0008).","pattern":"^[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])T(?:[01][0-9]|2[0-3]):[0-5][0-9]$"}; +const pattern253 = new RegExp("^[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])T(?:[01][0-9]|2[0-3]):[0-5][0-9]$", "u"); -function validate196(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate202(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate196.evaluated; +const evaluated0 = validate202.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -32996,7 +34950,7 @@ vErrors.push(err4); errors++; } if(!(data0 === "at")){ -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"}; +const err5 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema475.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err5]; } @@ -33044,7 +34998,7 @@ vErrors.push(err8); } errors++; } -if(!pattern59.test(data2)){ +if(!pattern66.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]; @@ -33069,7 +35023,7 @@ errors++; if(data.localTime !== undefined){ let data3 = data.localTime; if(typeof data3 === "string"){ -if(!pattern242.test(data3)){ +if(!pattern253.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]; @@ -33145,7 +35099,7 @@ vErrors.push(err16); errors++; } if(!(data4 === "on-approval")){ -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"}; +const err17 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema475.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err17]; } @@ -33201,22 +35155,22 @@ vErrors = null; } } } -validate196.errors = vErrors; +validate202.errors = vErrors; evaluated0.props = props0; return errors === 0; } -validate196.evaluated = {"dynamicProps":true,"dynamicItems":false}; +validate202.evaluated = {"dynamicProps":true,"dynamicItems":false}; -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"]}; +const schema481 = {"description":"Why the release may run, discriminated by `kind`.","oneOf":[{"type":"object","title":"ReleasePersonApproval","description":"A person approved these items (bound to `hash`) for `executeAt`, either by deciding an agent's request (`approvalId`) or by scheduling it themselves. The person's rights are checked again when the release runs.","additionalProperties":false,"required":["kind","by","at"],"properties":{"kind":{"type":"string","enum":["person"]},"by":{"$ref":"#/$defs/ReleasePerson"},"at":{"$ref":"#/$defs/Timestamp"},"executeAt":{"$ref":"#/$defs/Timestamp"},"approvalId":{"$ref":"#/$defs/Id"}}},{"type":"object","title":"ReleaseAutoApproval","description":"An agent key in `auto` mode scheduled a low-risk release without a person (ADR-0015). Valid only while the release runs before `windowEnds`: the end of the key's elevation, or 24 hours after scheduling, whichever comes first.","additionalProperties":false,"required":["kind","by","keyId","at","windowEnds"],"properties":{"kind":{"type":"string","enum":["auto"]},"by":{"$ref":"#/$defs/Actor"},"keyId":{"type":"string","pattern":"^[A-Za-z0-9_-]{6,64}$"},"at":{"$ref":"#/$defs/Timestamp"},"windowEnds":{"$ref":"#/$defs/Timestamp"}}}]}; +const schema482 = {"type":"object","description":"A staff person (`user_…`). Agents never appear here.","additionalProperties":false,"required":["id"],"properties":{"id":{"type":"string","pattern":"^user_[A-Za-z0-9]{6,32}$"},"name":{"type":"string","maxLength":200}}}; +const pattern258 = new RegExp("^[A-Za-z0-9_-]{6,64}$", "u"); +const schema486 = {"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}}; +const schema487 = {"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]}; -function validate199(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate205(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate199.evaluated; +const evaluated0 = validate205.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -33269,7 +35223,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: schema473.enum},message:"must be equal to one of the allowed values"}; +const err4 = {instancePath:instancePath+"/kind",schemaPath:"#/$defs/ActorKind/enum",keyword:"enum",params:{allowedValues: schema487.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err4]; } @@ -33360,16 +35314,16 @@ vErrors.push(err11); } errors++; } -validate199.errors = vErrors; +validate205.errors = vErrors; return errors === 0; } -validate199.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate205.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate198(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate204(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate198.evaluated; +const evaluated0 = validate204.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -33436,7 +35390,7 @@ vErrors.push(err4); errors++; } if(!(data0 === "person")){ -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"}; +const err5 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema481.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err5]; } @@ -33474,7 +35428,7 @@ errors++; if(data1.id !== undefined){ let data2 = data1.id; if(typeof data2 === "string"){ -if(!pattern221.test(data2)){ +if(!pattern232.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]; @@ -33722,7 +35676,7 @@ vErrors.push(err28); errors++; } if(!(data7 === "auto")){ -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"}; +const err29 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema481.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err29]; } @@ -33733,15 +35687,15 @@ errors++; } } if(data.by !== undefined){ -if(!(validate199(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate199.errors : vErrors.concat(validate199.errors); +if(!(validate205(data.by, {instancePath:instancePath+"/by",parentData:data,parentDataProperty:"by",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate205.errors : vErrors.concat(validate205.errors); errors = vErrors.length; } } if(data.keyId !== undefined){ let data9 = data.keyId; if(typeof data9 === "string"){ -if(!pattern247.test(data9)){ +if(!pattern258.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]; @@ -33859,20 +35813,20 @@ vErrors = null; } } } -validate198.errors = vErrors; +validate204.errors = vErrors; evaluated0.props = props0; return errors === 0; } -validate198.evaluated = {"dynamicProps":true,"dynamicItems":false}; +validate204.evaluated = {"dynamicProps":true,"dynamicItems":false}; -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"}}}; +const schema491 = {"description":"What happened when the release was due, discriminated by `kind` (it matches the terminal `state`).","oneOf":[{"type":"object","title":"ReleasePublishedOutcome","description":"Applied: `snapshotId` is the new live snapshot (reason `scheduled`), composed from `baseSnapshotId` and the items. `purge` says how the edge cache followed.","additionalProperties":false,"required":["kind","runId","at","snapshotId","baseSnapshotId","latenessSeconds","purge"],"properties":{"kind":{"type":"string","enum":["published"]},"runId":{"$ref":"#/$defs/Id"},"at":{"$ref":"#/$defs/Timestamp"},"snapshotId":{"$ref":"#/$defs/Id"},"baseSnapshotId":{"$ref":"#/$defs/Id"},"latenessSeconds":{"type":"integer","minimum":0},"purge":{"type":"string","enum":["tags","everything","deferred","failed","none"]}}},{"type":"object","title":"ReleaseConflictOutcome","description":"Held: these nodes changed since the release was pinned. Nothing was published.","additionalProperties":false,"required":["kind","at","conflicts"],"properties":{"kind":{"type":"string","enum":["conflict"]},"runId":{"$ref":"#/$defs/Id"},"at":{"$ref":"#/$defs/Timestamp"},"conflicts":{"type":"array","items":{"$ref":"#/$defs/ReleaseConflict"},"minItems":1,"maxItems":100}}},{"type":"object","title":"ReleaseFailedOutcome","description":"Refused at run time; nothing was published.","additionalProperties":false,"required":["kind","at","code"],"properties":{"kind":{"type":"string","enum":["failed"]},"runId":{"$ref":"#/$defs/Id"},"at":{"$ref":"#/$defs/Timestamp"},"code":{"type":"string","enum":["dependency_missing","invalid_snapshot","site_missing","node_body_missing","store_error"]},"message":{"type":"string","maxLength":500,"pattern":"^[^\\x00-\\x1F\\x7F\\u202A-\\u202E\\u2066-\\u2069]*$"}}},{"type":"object","title":"ReleaseMissedOutcome","description":"The scheduler reached the release more than an hour after its time; nothing was published. A person may publish it now.","additionalProperties":false,"required":["kind","at","latenessSeconds"],"properties":{"kind":{"type":"string","enum":["missed"]},"at":{"$ref":"#/$defs/Timestamp"},"latenessSeconds":{"type":"integer","minimum":0}}},{"type":"object","title":"ReleaseExpiredOutcome","description":"No valid approval when the release was due: never approved, the approver can no longer publish it, the agent's `auto` window ended, or its creator left.","additionalProperties":false,"required":["kind","at","reason"],"properties":{"kind":{"type":"string","enum":["expired"]},"at":{"$ref":"#/$defs/Timestamp"},"reason":{"type":"string","enum":["unapproved","approval_lost","auto_window_ended","creator_removed"]}}},{"type":"object","title":"ReleaseCanceledOutcome","description":"Withdrawn by a person or an agent; its nodes are unlocked.","additionalProperties":false,"required":["kind","at","by"],"properties":{"kind":{"type":"string","enum":["canceled"]},"at":{"$ref":"#/$defs/Timestamp"},"by":{"$ref":"#/$defs/Actor"}}}]}; +const pattern265 = new RegExp("^[^\\x00-\\x1F\\x7F\\u202A-\\u202E\\u2066-\\u2069]*$", "u"); +const schema498 = {"type":"object","description":"A node whose live version was not the pinned base when the release ran. `expected` absent: the node was not expected to be live; `found` absent: it was not live.","additionalProperties":false,"required":["nodeId"],"properties":{"nodeId":{"$ref":"#/$defs/Id"},"expected":{"$ref":"#/$defs/ReleaseNodeVersion"},"found":{"$ref":"#/$defs/ReleaseNodeVersion"}}}; -function validate203(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate209(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate203.evaluated; +const evaluated0 = validate209.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -33948,14 +35902,14 @@ errors++; } } if(data.expected !== undefined){ -if(!(validate191(data.expected, {instancePath:instancePath+"/expected",parentData:data,parentDataProperty:"expected",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate191.errors : vErrors.concat(validate191.errors); +if(!(validate197(data.expected, {instancePath:instancePath+"/expected",parentData:data,parentDataProperty:"expected",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate197.errors : vErrors.concat(validate197.errors); errors = vErrors.length; } } if(data.found !== undefined){ -if(!(validate191(data.found, {instancePath:instancePath+"/found",parentData:data,parentDataProperty:"found",rootData,dynamicAnchors}))){ -vErrors = vErrors === null ? validate191.errors : vErrors.concat(validate191.errors); +if(!(validate197(data.found, {instancePath:instancePath+"/found",parentData:data,parentDataProperty:"found",rootData,dynamicAnchors}))){ +vErrors = vErrors === null ? validate197.errors : vErrors.concat(validate197.errors); errors = vErrors.length; } } @@ -33970,16 +35924,16 @@ vErrors.push(err6); } errors++; } -validate203.errors = vErrors; +validate209.errors = vErrors; return errors === 0; } -validate203.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; +validate209.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; -function validate202(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ +function validate208(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){ let vErrors = null; let errors = 0; -const evaluated0 = validate202.evaluated; +const evaluated0 = validate208.evaluated; if(evaluated0.dynamicProps){ evaluated0.props = undefined; } @@ -34086,7 +36040,7 @@ vErrors.push(err8); errors++; } if(!(data0 === "published")){ -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"}; +const err9 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/0/properties/kind/enum",keyword:"enum",params:{allowedValues: schema491.oneOf[0].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err9]; } @@ -34294,7 +36248,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: schema477.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: schema491.oneOf[0].properties.purge.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err27]; } @@ -34378,7 +36332,7 @@ vErrors.push(err33); errors++; } if(!(data7 === "conflict")){ -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"}; +const err34 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/1/properties/kind/enum",keyword:"enum",params:{allowedValues: schema491.oneOf[1].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err34]; } @@ -34483,8 +36437,8 @@ errors++; } const len0 = data10.length; for(let i0=0; i0 { return out; } -/** The zone's offset from UTC at an instant, in milliseconds. */ -function offsetMs(zone: string, epochMs: number): number { +/** The zone's offset from UTC at an instant, in milliseconds (throws on an unknown zone). */ +export function offsetMs(zone: string, epochMs: number): number { const p = partsIn(zone, epochMs); const local = Date.UTC( p.year as number, diff --git a/packages/core-ts/src/fields/gate.ts b/packages/core-ts/src/fields/gate.ts new file mode 100644 index 00000000..0c7e9753 --- /dev/null +++ b/packages/core-ts/src/fields/gate.ts @@ -0,0 +1,119 @@ +/** + * RFC-0042 §3: the time gate an entry derives from a date or datetime field + * (`CollectionSettings.expiry`). One function for every caller — the renderer's `visible`, the + * gate index, search rows, Studio — so they agree to the millisecond. Pure, Worker-safe and + * Temporal-free (RFC-0008 §9.4): zones go through `Intl` only. + * + * - a `datetime` value expires at its `at`; + * - a `date` value expires at the start of the following day in the site's zone (absent: UTC), so + * an event "on 5 November" is listed all of 5 November; + * - plus `afterMinutes`; no value, no derived gate. + * + * Never early (RFC-0008 §9.1): an ambiguous wall-clock midnight takes the later instant, a + * midnight inside a gap takes the instant the wall clock would show it with the offset before the + * transition (Temporal's `disambiguation: "later"`), and an unknown zone takes the latest start of + * that day anywhere (UTC−12). + */ +import type { Availability, FieldDefinition } from "@cloudhal/core-contracts"; +import { offsetMs } from "./conversions.ts"; +import type { ResolvedFields } from "./resolve.ts"; +import type { SiteFields } from "./types.ts"; +import { isCalendarDay } from "./value-checks.ts"; + +const MINUTE = 60_000; +const HOUR = 60 * MINUTE; +/** The span around a wall time in which the zone's possible offsets are sampled. */ +const SAMPLE_SPAN = 36 * HOUR; +/** The most negative offset in use (Etc/GMT+12): the latest start of a day anywhere. */ +const LATEST_OFFSET = -12 * HOUR; + +/** Every offset the zone has within ±36 h of `wall` (as if it were UTC). */ +function offsetsNear(zone: string, wall: number): number[] { + const out = new Set(); + for (const t of [wall - SAMPLE_SPAN, wall, wall + SAMPLE_SPAN]) out.add(offsetMs(zone, t)); + return [...out]; +} + +/** + * The instant at which the wall clock in `zone` shows 00:00 of `day` (`YYYY-MM-DD`), in epoch ms. + * Ambiguous: the later instant; in a gap: shifted forward by the gap (`disambiguation: "later"`). + */ +export function dayStartIn(day: string, zone: string): number { + const [y, m, d] = day.split("-").map(Number) as [number, number, number]; + const wall = Date.UTC(y, m - 1, d); + let offsets: number[]; + try { + offsets = offsetsNear(zone, wall); + } catch { + return wall - LATEST_OFFSET; + } + const candidates = offsets.map((o) => wall - o); + const valid = candidates.filter((at, i) => offsetMs(zone, at) === offsets[i]); + return Math.max(...(valid.length > 0 ? valid : candidates)); +} + +/** The calendar day after `day` (`YYYY-MM-DD`). */ +export function nextDay(day: string): string { + const [y, m, d] = day.split("-").map(Number) as [number, number, number]; + const next = new Date(0); + next.setUTCFullYear(y, m - 1, d + 1); + const pad = (n: number, w = 2) => String(n).padStart(w, "0"); + return `${pad(next.getUTCFullYear(), 4)}-${pad(next.getUTCMonth() + 1)}-${pad(next.getUTCDate())}`; +} + +const isRecord = (v: unknown): v is Record => + typeof v === "object" && v !== null && !Array.isArray(v); + +/** + * The instant (epoch ms) at which a value of `def` ends, plus `afterMinutes`; `undefined` when + * there is no readable value or the field is neither a date nor a datetime. + */ +export function fieldGateInstant( + value: unknown, + def: Pick, + site: Pick, + afterMinutes = 0, +): number | undefined { + const grace = Number.isFinite(afterMinutes) && afterMinutes > 0 ? afterMinutes * MINUTE : 0; + if (def.type === "datetime") { + if (!isRecord(value) || typeof value.at !== "string") return undefined; + const at = Date.parse(value.at); + return Number.isFinite(at) ? at + grace : undefined; + } + if (def.type === "date") { + if (typeof value !== "string" || !isCalendarDay(value)) return undefined; + return dayStartIn(nextDay(value), site.timeZone ?? "UTC") + grace; + } + return undefined; +} + +/** + * The derived gate of an entry of `collection` from its resolved values (`resolveFields`, so a + * translation reads the original's non-localized date): `undefined` without a rule, without a + * date or datetime field under the rule's key, or without a value. + */ +export function expiryInstant( + site: SiteFields, + collection: string, + resolved: ResolvedFields, +): number | undefined { + const rule = site.collections?.[collection]?.expiry; + if (!rule) return undefined; + const field = resolved[rule.field]; + if (!field || (field.def.type !== "date" && field.def.type !== "datetime")) return undefined; + return fieldGateInstant(field.value, field.def, site, rule.afterMinutes ?? 0); +} + +/** + * The effective expiry (§3.2): `min(availability.until, derived)`. `NaN` when `until` does not + * parse, so gates keep failing closed as `gateState` does; `undefined` when neither exists. + */ +export function effectiveUntil( + availability: Availability | undefined, + derived: number | undefined, +): number | undefined { + const until = availability?.until === undefined ? undefined : Date.parse(availability.until); + if (until === undefined) return derived; + if (derived === undefined || Number.isNaN(until)) return until; + return Math.min(until, derived); +} diff --git a/packages/core-ts/src/fields/index.ts b/packages/core-ts/src/fields/index.ts index 67717985..ff41ce17 100644 --- a/packages/core-ts/src/fields/index.ts +++ b/packages/core-ts/src/fields/index.ts @@ -2,7 +2,8 @@ * `@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. + * receive, and the values readers see (`resolveFields`); RFC-0042's listings by field (`query.ts`) + * and derived expiry gates (`gate.ts`). Worker-safe and Temporal-free. */ export { type Conversion, @@ -21,6 +22,7 @@ export { fieldDefinitionChanges, } from "./definitions.ts"; export { type FormatFieldOptions, formatFieldValue } from "./format.ts"; +export { dayStartIn, effectiveUntil, expiryInstant, fieldGateInstant, nextDay } from "./gate.ts"; export { fieldJsonSchema, fieldsJsonSchema, @@ -45,6 +47,23 @@ export { fieldsPropsSchema, INSTANT_PATTERN, } from "./props.ts"; +export { + canOrderBy, + checkFieldQuery, + compareByField, + FIELD_QUERY_LIMITS, + FIELD_QUERY_OPS, + type FieldOperand, + type FieldQuery, + type FieldQueryIssue, + type FieldQueryOp, + matchesFieldQuery, + NOW, + type ParsedCondition, + type ParsedFieldQuery, + type ParseFieldQueryResult, + parseFieldQuery, +} from "./query.ts"; export { fieldRefusalCode, refuseFieldIssues } from "./refusal.ts"; export { type FieldsEntry, diff --git a/packages/core-ts/src/fields/query.ts b/packages/core-ts/src/fields/query.ts new file mode 100644 index 00000000..e706c816 --- /dev/null +++ b/packages/core-ts/src/fields/query.ts @@ -0,0 +1,371 @@ +/** + * RFC-0042 §4: listings by field. One evaluator for every caller — themes (`queryEntries`), + * `contentList` sections, the API, MCP and Studio's list columns — following `filter.ts`'s pattern + * (one parser, one meaning). It runs in memory after the store's collection prefilter, so SQL and + * in-memory results cannot drift. Pure, Worker-safe and Temporal-free. + * + * | Field type | `where` ops | `orderBy` | `value` | + * |---|---|---|---| + * | number | eq ne lt lte gt gte set unset | yes | decimal | + * | date | same | yes | `YYYY-MM-DD` or `now` (today in the site zone) | + * | datetime | same (compares `at`) | yes | RFC 3339 instant or `now` | + * | boolean | eq (absent reads false) | no | `true`/`false` | + * | select | eq ne in set unset (`multiple`: contains / contains any) | no | option value(s) | + * | text, url | eq set unset | no | exact string | + * | richText, image | set unset | no | none | + * + * Conditions are ANDed. `ne` is "not `eq`", so entries without a value match it. Only listed + * fields (`CollectionSettings.listFields`) may be queried. Entries without a value sort last in + * both directions; ties are left to the caller (the collection's `sort`, then `id`). + */ +import type { FieldCondition, FieldDefinition, FieldOrder } from "@cloudhal/core-contracts"; +import { dayIn } from "./conversions.ts"; +import { isBeingRemoved } from "./migration.ts"; +import { hasFieldValue, type ResolvedFields } from "./resolve.ts"; +import { type FieldType, fieldDefinitions, type SiteFields } from "./types.ts"; +import { isCalendarDay } from "./value-checks.ts"; + +export type FieldQueryOp = FieldCondition["op"]; + +/** A listing's field conditions and order, as sections, the API and MCP carry them. */ +export interface FieldQuery { + readonly where?: readonly FieldCondition[] | undefined; + readonly orderBy?: FieldOrder | undefined; +} + +/** One reason a query is refused (`invalid_query`), at a slash path (`where/0/value`). */ +export interface FieldQueryIssue { + path: string; + code: "invalid_query"; + message: string; +} + +/** RFC-0042 Security: the bounds of a query (also in the schema). */ +export const FIELD_QUERY_LIMITS = { + conditions: 4, + values: 20, + valueLength: 100, +} as const; + +const ORDERED = ["eq", "ne", "lt", "lte", "gt", "gte", "set", "unset"] as const; + +/** The closed operator set per field type. */ +export const FIELD_QUERY_OPS: Readonly> = { + number: ORDERED, + date: ORDERED, + datetime: ORDERED, + boolean: ["eq"], + select: ["eq", "ne", "in", "set", "unset"], + text: ["eq", "set", "unset"], + url: ["eq", "set", "unset"], + richText: ["set", "unset"], + image: ["set", "unset"], +}; + +/** True when a listing may be ordered by a field of this type. */ +export const canOrderBy = (type: FieldType): boolean => + type === "number" || type === "date" || type === "datetime"; + +/** The value `now` (the only relative value; no offsets in stage 3). */ +export const NOW = "now"; + +const DECIMAL = /^-?(?:\d+(?:\.\d+)?|\.\d+)$/; +const INSTANT = + /^(\d{4}-\d{2}-\d{2})T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d{1,9})?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/; + +/** A condition value read by its field's type. */ +export type FieldOperand = + | { kind: "number"; n: number } + | { kind: "day"; day: string } + | { kind: "instant"; ms: number } + | { kind: "now" } + | { kind: "string"; s: string }; + +/** Reads one `value` by the field's type; a string describing the problem when it does not fit. */ +function readValue(def: FieldDefinition, raw: string): FieldOperand | string { + switch (def.type) { + case "number": + return DECIMAL.test(raw) ? { kind: "number", n: Number(raw) } : "a decimal number"; + case "date": + if (raw === NOW) return { kind: "now" }; + return isCalendarDay(raw) ? { kind: "day", day: raw } : "YYYY-MM-DD or now"; + case "datetime": { + if (raw === NOW) return { kind: "now" }; + const day = INSTANT.exec(raw)?.[1]; + const ms = day && isCalendarDay(day) ? Date.parse(raw) : Number.NaN; + return Number.isFinite(ms) ? { kind: "instant", ms } : "an RFC 3339 instant or now"; + } + case "boolean": + return raw === "true" || raw === "false" ? { kind: "string", s: raw } : "true or false"; + case "select": + return def.options.some((o) => o.value === raw) + ? { kind: "string", s: raw } + : `one of ${def.options.map((o) => o.value).join(", ")}`; + default: + return { kind: "string", s: raw }; + } +} + +/** A checked condition, its operands read by type. */ +export interface ParsedCondition { + readonly key: string; + readonly def: FieldDefinition; + readonly op: FieldQueryOp; + readonly operands: readonly FieldOperand[]; +} + +/** A checked query, ready for `matchesFieldQuery` and `compareByField`. */ +export interface ParsedFieldQuery { + readonly where: readonly ParsedCondition[]; + readonly orderBy?: { readonly def: FieldDefinition; readonly direction: "asc" | "desc" }; + /** The site's zone, for `now` on date fields (today there). */ + readonly timeZone: string; +} + +const issue = (path: string, message: string): FieldQueryIssue => ({ + path, + code: "invalid_query", + message, +}); + +/** The listed definition of `key`, or the reason it cannot be queried. */ +function listedDef( + byKey: ReadonlyMap, + listFields: readonly string[] | undefined, + key: unknown, +): FieldDefinition | string { + if (typeof key !== "string") return "names no field"; + const def = byKey.get(key); + if (!def) return `"${key}" is not a field of this collection`; + if (!listFields?.includes(key)) return `"${key}" is not one of the collection's list fields`; + return def; +} + +function checkOperands(c: FieldCondition, def: FieldDefinition, at: string): FieldQueryIssue[] { + const bare = c.op === "set" || c.op === "unset"; + if (bare) + return c.value !== undefined || c.values !== undefined + ? [issue(at, `${c.op} takes no value`)] + : []; + if (c.op === "in") { + // Callers pass JSON from the API and MCP: read it as untyped. + const values = c.values as readonly unknown[] | undefined; + if (c.value !== undefined) return [issue(`${at}/value`, "in takes values, not value")]; + if (!Array.isArray(values) || values.length === 0) + return [issue(`${at}/values`, "in needs at least one value")]; + if (values.length > FIELD_QUERY_LIMITS.values) + return [issue(`${at}/values`, `at most ${FIELD_QUERY_LIMITS.values} values`)]; + if (new Set(values).size !== values.length) return [issue(`${at}/values`, "values are unique")]; + return values.flatMap((v, i) => checkString(def, v, `${at}/values/${i}`)); + } + if (c.values !== undefined) return [issue(`${at}/values`, "values only with in")]; + if (c.value === undefined) return [issue(`${at}/value`, `${c.op} needs a value`)]; + return checkString(def, c.value, `${at}/value`); +} + +function checkString(def: FieldDefinition, raw: unknown, at: string): FieldQueryIssue[] { + if (typeof raw !== "string" || raw.length === 0 || raw.length > FIELD_QUERY_LIMITS.valueLength) + return [issue(at, `a string of 1 to ${FIELD_QUERY_LIMITS.valueLength} characters`)]; + const read = readValue(def, raw); + return typeof read === "string" ? [issue(at, `a ${def.type} field compares with ${read}`)] : []; +} + +function checkCondition( + c: FieldCondition, + i: number, + byKey: ReadonlyMap, + listFields: readonly string[] | undefined, +): FieldQueryIssue[] { + const at = `where/${i}`; + const def = listedDef(byKey, listFields, c?.field); + if (typeof def === "string") return [issue(`${at}/field`, def)]; + const ops = FIELD_QUERY_OPS[def.type]; + if (!ops.includes(c.op)) + return [issue(`${at}/op`, `a ${def.type} field takes ${ops.join(", ")}`)]; + return checkOperands(c, def, at); +} + +/** + * Every reason `query` cannot run against these definitions and list fields (`invalid_query`): + * unknown or unlisted fields, operators outside the type's set, values that do not read as the + * field's type, `values` without `in`, an order by a type that does not sort, and the limits. + */ +export function checkFieldQuery( + defs: readonly FieldDefinition[], + listFields: readonly string[] | undefined, + query: FieldQuery, +): FieldQueryIssue[] { + const byKey = new Map(defs.filter((d) => !isBeingRemoved(d)).map((d) => [d.key, d])); + const out: FieldQueryIssue[] = []; + const where = query.where ?? []; + if (!Array.isArray(where)) return [issue("where", "a list of conditions")]; + if (where.length > FIELD_QUERY_LIMITS.conditions) + out.push(issue("where", `at most ${FIELD_QUERY_LIMITS.conditions} conditions`)); + where.forEach((c, i) => { + out.push(...checkCondition(c, i, byKey, listFields)); + }); + const order = query.orderBy; + if (order !== undefined) { + const def = listedDef(byKey, listFields, order?.field); + if (typeof def === "string") out.push(issue("orderBy/field", def)); + else if (!canOrderBy(def.type)) + out.push(issue("orderBy/field", `a ${def.type} field does not sort`)); + if (order?.direction !== "asc" && order?.direction !== "desc") + out.push(issue("orderBy/direction", "asc or desc")); + } + return out; +} + +export type ParseFieldQueryResult = + | { ok: true; query: ParsedFieldQuery } + | { ok: false; issues: FieldQueryIssue[] }; + +/** Checks `query` against `collection`'s definitions and list fields, and compiles it. */ +export function parseFieldQuery( + site: SiteFields, + collection: string, + query: FieldQuery, +): ParseFieldQueryResult { + const defs = fieldDefinitions(site, collection); + const listFields = site.collections?.[collection]?.listFields; + const issues = checkFieldQuery(defs, listFields, query); + if (issues.length > 0) return { ok: false, issues }; + const byKey = new Map(defs.map((d) => [d.key, d])); + const where = (query.where ?? []).map((c): ParsedCondition => { + const def = byKey.get(c.field) as FieldDefinition; + const raws = c.op === "in" ? (c.values ?? []) : c.value === undefined ? [] : [c.value]; + return { + key: c.field, + def, + op: c.op, + operands: raws.map((r) => readValue(def, r) as FieldOperand), + }; + }); + const order = query.orderBy; + const parsed: ParsedFieldQuery = { + where, + timeZone: site.timeZone ?? "UTC", + ...(order ? { orderBy: { def: byKey.get(order.field) as FieldDefinition, ...order } } : {}), + }; + return { ok: true, query: parsed }; +} + +/** A comparable form of a resolved value: a number, a day string, or undefined. */ +function sortKey(def: FieldDefinition, value: unknown): number | string | undefined { + if (value === undefined || value === null) return undefined; + if (def.type === "number") return typeof value === "number" ? value : undefined; + if (def.type === "date") return typeof value === "string" ? value : undefined; + if (def.type === "datetime") { + const at = (value as { at?: unknown }).at; + const ms = typeof at === "string" ? Date.parse(at) : Number.NaN; + return Number.isFinite(ms) ? ms : undefined; + } + return undefined; +} + +/** An operand's comparable form at `now`. */ +function operandKey( + o: FieldOperand, + def: FieldDefinition, + now: number, + zone: string, +): number | string { + switch (o.kind) { + case "number": + return o.n; + case "day": + return o.day; + case "instant": + return o.ms; + case "now": + return def.type === "date" ? dayIn(new Date(now).toISOString(), zone) : now; + default: + return o.s; + } +} + +type Compare = (a: number | string, b: number | string) => boolean; +const COMPARE: Partial> = { + eq: (a, b) => a === b, + lt: (a, b) => a < b, + lte: (a, b) => a <= b, + gt: (a, b) => a > b, + gte: (a, b) => a >= b, +}; + +/** The option values a select holds (one, or a list when `multiple`). */ +const selected = (value: unknown): string[] => + Array.isArray(value) + ? value.filter((v): v is string => typeof v === "string") + : typeof value === "string" + ? [value] + : []; + +function matchesEq(c: ParsedCondition, value: unknown, now: number, zone: string): boolean { + const operand = c.operands[0] as FieldOperand; + const { def } = c; + if (def.type === "boolean") + return (value === true) === (operand.kind === "string" && operand.s === "true"); + if (def.type === "select") + return selected(value).includes(operandKey(operand, def, now, zone) as string); + if (def.type === "text" || def.type === "url") + return value === operandKey(operand, def, now, zone); + const key = sortKey(def, value); + return key !== undefined && key === operandKey(operand, def, now, zone); +} + +function matchesCondition(c: ParsedCondition, value: unknown, now: number, zone: string): boolean { + switch (c.op) { + case "set": + return hasFieldValue(value); + case "unset": + return !hasFieldValue(value); + case "eq": + return matchesEq(c, value, now, zone); + case "ne": + return !matchesEq(c, value, now, zone); + case "in": { + const held = selected(value); + return c.operands.some((o) => o.kind === "string" && held.includes(o.s)); + } + default: { + const key = sortKey(c.def, value); + const compare = COMPARE[c.op] as Compare; + return ( + key !== undefined && + compare(key, operandKey(c.operands[0] as FieldOperand, c.def, now, zone)) + ); + } + } +} + +/** + * True when an entry's resolved values (`resolveFields`, so a translation reads the original's + * non-localized values) meet every condition at `now` (epoch ms, injected; never a clock read). + */ +export function matchesFieldQuery( + resolved: ResolvedFields, + query: ParsedFieldQuery, + now: number, +): boolean { + return query.where.every((c) => matchesCondition(c, resolved[c.key]?.value, now, query.timeZone)); +} + +/** + * Orders two entries' resolved values by the query's `orderBy`: negative when `a` comes first. + * Entries without a value come last in both directions; `0` on a tie (or without `orderBy`), so + * the caller falls back to its `sort`, then `id`. + */ +export function compareByField( + a: ResolvedFields, + b: ResolvedFields, + query: Pick, +): number { + const order = query.orderBy; + if (!order) return 0; + const ka = sortKey(order.def, a[order.def.key]?.value); + const kb = sortKey(order.def, b[order.def.key]?.value); + if (ka === undefined || kb === undefined) return ka === kb ? 0 : ka === undefined ? 1 : -1; + const sign = order.direction === "asc" ? 1 : -1; + return ka < kb ? -sign : ka > kb ? sign : 0; +} diff --git a/packages/core-ts/src/structured-data/build.ts b/packages/core-ts/src/structured-data/build.ts new file mode 100644 index 00000000..4b4c67f5 --- /dev/null +++ b/packages/core-ts/src/structured-data/build.ts @@ -0,0 +1,182 @@ +/** + * RFC-0042 §2 "Per entry": `structuredDataFor` builds the preset document of one entry from its + * resolved values (`resolveFields`, so a translation reads the original's non-localized values), + * the entry and the site. Pure: the caller resolves asset URLs and the entry's URL. + * + * - When a required value is missing, no document is emitted (Google treats an incomplete item as + * invalid); the issues say which (`Structured data: startDate missing`). + * - A document is capped at 32 KB serialized: optional properties are dropped first, in reverse + * table order, `description` last; a required `description` is shortened instead. + * - The result is a plain object; the renderer serializes it with `jsonLdText`. + */ +import type { StructuredDataPreset, StructuredDataSource } from "@cloudhal/core-contracts"; +import type { ResolvedFields } from "../fields/resolve.ts"; +import type { StructuredDataIssue } from "./check.ts"; +import { + KIND_SOURCES, + type PathSpec, + PRESETS, + type PresetSpec, + requiredPaths, + STRUCTURED_DATA_MAX_BYTES, +} from "./table.ts"; +import { + type ConvertInput, + type JsonLdValue, + type StructuredDataContext, + type StructuredDataEntry, + type StructuredDataSite, + sourceValue, +} from "./values.ts"; + +export type JsonLdDocument = { [k: string]: JsonLdValue }; + +export interface StructuredDataResult { + /** The preset document, absent without a preset or when a required value is missing. */ + doc?: JsonLdDocument; + issues: StructuredDataIssue[]; +} + +/** The source a path reads: the mapping's, else the table's default. */ +const sourceOf = (preset: StructuredDataPreset, path: string, spec: PathSpec) => + preset.properties[path] ?? spec.default; + +/** A field source whose field no longer fits the path (its type changed) gives nothing. */ +function fits(source: StructuredDataSource, resolved: ResolvedFields, spec: PathSpec): boolean { + if (source.kind !== "field") return true; + const def = resolved[source.field]?.def; + if (!def) return false; + return [...KIND_SOURCES[spec.kind].fields, ...(spec.alsoFields ?? [])].includes(def.type); +} + +/** Every path's value, in table order. */ +function collect( + preset: StructuredDataPreset, + table: PresetSpec, + resolved: ResolvedFields, + base: Omit, +): Map { + const values = new Map(); + for (const [path, spec] of Object.entries(table.paths)) { + const source = sourceOf(preset, path, spec); + if (!source || !fits(source, resolved, spec)) continue; + const field = source.kind === "field" ? resolved[source.field] : undefined; + const value = sourceValue(source, field, { ...base, spec }); + if (value !== undefined) values.set(path, value); + } + return values; +} + +const TELECOMMUTE = "TELECOMMUTE"; + +/** The required paths without a value (a remote job needs no address). */ +function missing(type: StructuredDataPreset["type"], values: Map): string[] { + const { paths, groups } = requiredPaths(type); + const out = paths.filter((p) => !values.has(p)); + const remote = + type === "JobPosting" && + values.get("jobLocationType") === TELECOMMUTE && + values.has("applicantLocationRequirements.name"); + for (const group of groups) + if (!remote && !group.some((p) => values.has(p))) + out.push(group[0]?.replace(/\.[^.]+$/, "") ?? ""); + return out; +} + +/** The document: nested objects get their `@type` from the table when created. */ +function assemble( + type: string, + table: PresetSpec, + values: Map, +): JsonLdDocument { + const doc: JsonLdDocument = { "@context": "https://schema.org", "@type": type }; + for (const [path, value] of values) { + const target = (table.paths[path]?.target ?? path).split("."); + let node = doc; + let prefix = ""; + for (const part of target.slice(0, -1)) { + prefix = prefix ? `${prefix}.${part}` : part; + const existing = node[part]; + if (typeof existing !== "object" || existing === null || Array.isArray(existing)) { + const objectType = table.types[prefix]; + node[part] = objectType ? { "@type": objectType } : {}; + } + node = node[part] as JsonLdDocument; + } + node[target[target.length - 1] as string] = value; + } + return doc; +} + +const encoder = new TextEncoder(); +const bytes = (doc: JsonLdDocument) => encoder.encode(JSON.stringify(doc)).length; + +/** Drops optional values (description last) or shortens a required description to fit the cap. */ +function fitCap( + type: StructuredDataPreset["type"], + table: PresetSpec, + values: Map, +): JsonLdDocument | undefined { + let doc = assemble(type, table, values); + if (bytes(doc) <= STRUCTURED_DATA_MAX_BYTES) return doc; + const optional = [...values.keys()] + .filter((p) => !table.paths[p]?.required && p !== "description") + .reverse(); + if (values.has("description") && !table.paths.description?.required) optional.push("description"); + for (const path of optional) { + values.delete(path); + doc = assemble(type, table, values); + if (bytes(doc) <= STRUCTURED_DATA_MAX_BYTES) return doc; + } + const description = values.get("description"); + if (typeof description !== "string") return undefined; + const over = bytes(doc) - STRUCTURED_DATA_MAX_BYTES; + // Each character costs at least one byte; trimming by the overshoot (plus an ellipsis) is enough + // after at most a few rounds, since multi-byte characters only shorten it faster. + let text = description; + for (let cut = over + 3; text.length > 0; cut *= 2) { + text = `${description.slice(0, Math.max(0, description.length - cut)).trimEnd()}…`; + values.set("description", text); + doc = assemble(type, table, values); + if (bytes(doc) <= STRUCTURED_DATA_MAX_BYTES) return doc; + if (text === "…") break; + } + return undefined; +} + +/** + * The JSON-LD document of an entry under its collection's preset, or none: without a preset, when + * a required value is missing, or when even the required properties exceed 32 KB. + */ +export function structuredDataFor( + site: StructuredDataSite, + resolved: ResolvedFields, + entry: StructuredDataEntry, + ctx: StructuredDataContext, +): StructuredDataResult { + const preset = site.collections?.[entry.collection]?.structuredData; + const table = preset ? PRESETS[preset.type] : undefined; + if (!preset || !table) return { issues: [] }; + const values = collect(preset, table, resolved, { site, entry, ctx }); + const absent = missing(preset.type, values); + if (absent.length > 0) + return { + issues: absent.map((path) => ({ + path, + code: "required_value_missing", + message: `Structured data: ${path} missing`, + })), + }; + const doc = fitCap(preset.type, table, values); + if (!doc) + return { + issues: [ + { + path: "", + code: "too_large", + message: `Structured data: over ${STRUCTURED_DATA_MAX_BYTES} bytes without optional properties`, + }, + ], + }; + return { doc, issues: [] }; +} diff --git a/packages/core-ts/src/structured-data/check.ts b/packages/core-ts/src/structured-data/check.ts new file mode 100644 index 00000000..7e98b745 --- /dev/null +++ b/packages/core-ts/src/structured-data/check.ts @@ -0,0 +1,229 @@ +/** + * RFC-0042 §2 "Checks when the preset is saved": `checkStructuredData(site)` for `updateSite`, the + * API and MCP's precheck (refused as `structured_data_invalid` with these issues). Errors refuse + * the save; warnings (select options outside a vocabulary) are reported and allowed. + */ +import type { + FieldDefinition, + StructuredDataPreset, + StructuredDataSource, +} from "@cloudhal/core-contracts"; +import { isBeingRemoved } from "../fields/migration.ts"; +import type { SiteFields } from "../fields/types.ts"; +import { + KIND_SOURCES, + type PathSpec, + PRESETS, + REMOTE_JOB_PATHS, + requiredPaths, + vocabularyEntry, +} from "./table.ts"; + +export type StructuredDataIssueCode = + /** A path the type's table does not offer. */ + | "unknown_path" + /** A field source names no field of this collection. */ + | "unknown_field" + /** A source that does not fit the path (field type, entry or site property, constant). */ + | "source_mismatch" + /** A constant or select option outside the path's vocabulary. */ + | "vocabulary" + /** A currency constant that is not an ISO 4217 code. */ + | "currency" + /** A required path is neither mapped nor defaulted. */ + | "required_missing" + /** A required path mapped to a constant: required values come from the entry. */ + | "constant_required" + /** Per entry: a required value is missing, so no preset document is emitted. */ + | "required_value_missing" + /** Per entry: the document is over 32 KB even without its optional properties. */ + | "too_large"; + +/** One problem, at a slash path (`collections/events/structuredData/properties/startDate`). */ +export interface StructuredDataIssue { + path: string; + code: StructuredDataIssueCode; + message: string; + /** Reported, not refused. */ + warning?: true; +} + +const CURRENCY = /^[A-Z]{3}$/; + +interface PathContext { + readonly at: string; + readonly path: string; + readonly spec: PathSpec; + readonly defs: ReadonlyMap; +} + +const err = ( + path: string, + code: StructuredDataIssueCode, + message: string, +): StructuredDataIssue => ({ + path, + code, + message, +}); + +function checkField(ctx: PathContext, key: string): StructuredDataIssue[] { + const { at, path, spec } = ctx; + const def = ctx.defs.get(key); + if (!def) + return [err(`${at}/field`, "unknown_field", `"${key}" is not a field of this collection`)]; + const fits = [...KIND_SOURCES[spec.kind].fields, ...(spec.alsoFields ?? [])]; + if (!fits.includes(def.type)) + return [ + err( + `${at}/field`, + "source_mismatch", + `${path} takes ${fits.join(" or ")} fields, not ${def.type}`, + ), + ]; + const multiline = def.type === "text" && def.multiline === true; + if ((spec.kind === "lines" || spec.kind === "steps") && def.type === "text" && !multiline) + return [err(`${at}/field`, "source_mismatch", `${path} takes a multiline text field`)]; + if (def.type !== "select") return []; + if (def.multiple && !spec.many) + return [err(`${at}/field`, "source_mismatch", `${path} takes a single select`)]; + const vocabulary = spec.vocabulary; + if (!vocabulary) return []; + return def.options + .filter((o) => !vocabularyEntry(vocabulary, o.value)) + .map((o) => ({ + ...err(`${at}/field`, "vocabulary", `option "${o.value}" emits nothing for ${path}`), + warning: true as const, + })); +} + +function checkConstant(ctx: PathContext, value: string): StructuredDataIssue[] { + const { at, path, spec } = ctx; + const sources = KIND_SOURCES[spec.kind]; + if (!sources.constant) + return [err(`${at}/value`, "source_mismatch", `${path} does not take a constant`)]; + if (spec.required) + return [ + err( + `${at}/value`, + "constant_required", + `${path} is required: its value comes from the entry`, + ), + ]; + if (spec.kind === "currency" && !CURRENCY.test(value)) + return [err(`${at}/value`, "currency", `${path} takes an ISO 4217 code such as EUR`)]; + if (spec.vocabulary && !vocabularyEntry(spec.vocabulary, value)) + return [err(`${at}/value`, "vocabulary", `"${value}" is not a value of ${path}`)]; + return []; +} + +function checkSource(ctx: PathContext, source: StructuredDataSource): StructuredDataIssue[] { + const { at, path, spec } = ctx; + const sources = KIND_SOURCES[spec.kind]; + switch (source.kind) { + case "field": + return checkField(ctx, source.field); + case "entry": + return sources.entry.includes(source.property) + ? [] + : [ + err( + `${at}/property`, + "source_mismatch", + `${path} does not take the entry's ${source.property}`, + ), + ]; + case "site": + return sources.site.includes(source.property) + ? [] + : [ + err( + `${at}/property`, + "source_mismatch", + `${path} does not take the site's ${source.property}`, + ), + ]; + case "text": + return checkConstant(ctx, source.value); + default: + return [err(at, "source_mismatch", "an unknown source")]; + } +} + +/** True when the path is mapped or has a default source. */ +const provided = (preset: StructuredDataPreset, path: string, spec: PathSpec | undefined) => + preset.properties[path] !== undefined || spec?.default !== undefined; + +function checkRequired(preset: StructuredDataPreset, at: string): StructuredDataIssue[] { + const table = PRESETS[preset.type]; + const { paths, groups } = requiredPaths(preset.type); + const out: StructuredDataIssue[] = []; + for (const path of paths) + if (!provided(preset, path, table.paths[path])) + out.push(err(`${at}/${path}`, "required_missing", `${preset.type} needs ${path}`)); + const remote = + preset.type === "JobPosting" && + REMOTE_JOB_PATHS.every((p) => preset.properties[p] !== undefined); + for (const group of groups) { + if (remote) continue; + const fromEntry = group.some((p) => { + const s = preset.properties[p]; + return s !== undefined && s.kind !== "text"; + }); + if (!fromEntry) { + const prefix = group[0]?.replace(/\.[^.]+$/, "") ?? ""; + out.push( + err( + `${at}/${prefix}`, + "required_missing", + `${preset.type} needs one of ${group.join(", ")} from a field`, + ), + ); + } + } + return out; +} + +/** Every issue of one collection's preset (`at` is the preset's slash path). */ +export function checkPreset( + preset: StructuredDataPreset, + defs: readonly FieldDefinition[], + at: string, +): StructuredDataIssue[] { + const table = PRESETS[preset.type]; + if (!table) return [err(`${at}/type`, "unknown_path", `"${String(preset.type)}" is not offered`)]; + const byKey = new Map(defs.filter((d) => !isBeingRemoved(d)).map((d) => [d.key, d])); + const out: StructuredDataIssue[] = []; + for (const [path, source] of Object.entries(preset.properties)) { + if (source === undefined) continue; + const propAt = `${at}/properties/${path}`; + const spec = Object.hasOwn(table.paths, path) ? table.paths[path] : undefined; + if (!spec) { + out.push(err(propAt, "unknown_path", `${preset.type} has no property ${path}`)); + continue; + } + out.push(...checkSource({ at: propAt, path, spec, defs: byKey }, source)); + } + out.push(...checkRequired(preset, `${at}/properties`)); + return out; +} + +/** + * Every structured-data issue of a site's collections: paths in the type's table, field sources + * naming a field of the collection whose type fits, select options and constants in the path's + * vocabulary (unmatched options are warnings), ISO 4217 currency constants, no constant on a + * required path, and every required path mapped or defaulted. + */ +export function checkStructuredData(site: SiteFields): StructuredDataIssue[] { + const out: StructuredDataIssue[] = []; + for (const [key, settings] of Object.entries(site.collections ?? {})) { + const preset = settings?.structuredData; + if (!preset) continue; + out.push(...checkPreset(preset, settings.fields ?? [], `collections/${key}/structuredData`)); + } + return out; +} + +/** The issues that refuse a save (warnings dropped). */ +export const structuredDataErrors = (issues: readonly StructuredDataIssue[]) => + issues.filter((i) => !i.warning); diff --git a/packages/core-ts/src/structured-data/index.ts b/packages/core-ts/src/structured-data/index.ts new file mode 100644 index 00000000..0cfdabda --- /dev/null +++ b/packages/core-ts/src/structured-data/index.ts @@ -0,0 +1,45 @@ +/** + * `@cloudhal/core-ts/structured-data` (RFC-0042 §2 / ADR-0044): JSON-LD presets mapped from a + * collection's typed fields. The closed table of `Event`, `JobPosting`, `Product` and `Recipe` + * (paths, fitting sources, required properties, vocabularies), the save-time check + * (`checkStructuredData`) and the per-entry builder (`structuredDataFor`). Pure, Worker-safe, + * eval-free and Temporal-free. + */ +export { + type JsonLdDocument, + type StructuredDataResult, + structuredDataFor, +} from "./build.ts"; +export { + checkPreset, + checkStructuredData, + type StructuredDataIssue, + type StructuredDataIssueCode, + structuredDataErrors, +} from "./check.ts"; +export { + KIND_SOURCES, + type KindSources, + type PathSpec, + PRESET_TYPES, + PRESETS, + type PresetSpec, + type PresetType, + REMOTE_JOB_PATHS, + requiredPaths, + STRUCTURED_DATA_MAX_BYTES, + type ValueKind, + VOCABULARIES, + type VocabularyEntry, + type VocabularyName, + vocabularyEntry, +} from "./table.ts"; +export { + absoluteHttpUrl, + bodyText, + type JsonLdValue, + type StructuredDataContext, + type StructuredDataEntry, + type StructuredDataSite, + withOffset, +} from "./values.ts"; diff --git a/packages/core-ts/src/structured-data/table.ts b/packages/core-ts/src/structured-data/table.ts new file mode 100644 index 00000000..9744b495 --- /dev/null +++ b/packages/core-ts/src/structured-data/table.ts @@ -0,0 +1,311 @@ +/** + * RFC-0042 §2: the closed preset table. Which paths each JSON-LD type accepts, which are required + * by Google (R), the default source of an unmapped path, which value sources fit each path, and the + * vocabularies of enum-valued properties. The contract bounds the shape; this table gives it + * meaning, so a change in Google's documentation is a change here, not in the schema. + * + * Sources (checked 2026-10-23): Google Search Central's Event, JobPosting, Product snippet and + * Recipe pages. `aggregateRating` and `review` are deliberately absent (self-serving review markup + * is against Google's policy, and Cloudhal stores no reviews). + */ +import type { + EntryValueProperty, + SiteValueProperty, + StructuredDataPreset, + StructuredDataSource, +} from "@cloudhal/core-contracts"; +import type { FieldType } from "../fields/types.ts"; + +export type PresetType = StructuredDataPreset["type"]; + +/** What a path's value is; decides the sources that fit and the conversion. */ +export type ValueKind = + /** Short plain text. */ + | "text" + /** Plain text that may be long (descriptions). */ + | "longText" + /** A finite number (prices, salaries). */ + | "number" + /** A date (`YYYY-MM-DD`) or an instant with its offset. */ + | "date" + /** An absolute http(s) URL. */ + | "url" + /** An absolute URL of an image. */ + | "image" + /** An ISO 4217 code. */ + | "currency" + /** A value of the path's vocabulary. */ + | "enum" + | "boolean" + /** An integer number of minutes → `PT{n}M`. */ + | "duration" + /** A multiline text field → one string per line. */ + | "lines" + /** Rich text or multiline text → one `HowToStep` per paragraph. */ + | "steps"; + +export interface PathSpec { + readonly kind: ValueKind; + /** Google-required. */ + readonly required?: true; + /** "At least one of" a group (an address) is required. */ + readonly group?: string; + /** The source used when the path is unmapped. */ + readonly default?: StructuredDataSource; + /** Enum paths: the vocabulary's name. */ + readonly vocabulary?: VocabularyName; + /** Field types beyond the kind's own (e.g. `recipeYield` from a number). */ + readonly alsoFields?: readonly FieldType[]; + /** Where the value goes when it differs from the path (`baseSalary.value` → its QuantitativeValue). */ + readonly target?: string; + /** A `multiple` select gives a list (Google accepts several `employmentType`s). */ + readonly many?: true; +} + +export interface PresetSpec { + readonly paths: Readonly>; + /** `@type` of each nested object, by its path. */ + readonly types: Readonly>; +} + +/** What each kind accepts: field types, entry and site properties, and whether constants fit. */ +export interface KindSources { + readonly fields: readonly FieldType[]; + readonly entry: readonly EntryValueProperty[]; + readonly site: readonly SiteValueProperty[]; + readonly constant: boolean; +} + +export const KIND_SOURCES: Readonly> = { + text: { fields: ["text"], entry: ["title", "summary"], site: ["name"], constant: true }, + longText: { + fields: ["text", "richText"], + entry: ["title", "summary", "body"], + site: ["name"], + constant: true, + }, + number: { fields: ["number"], entry: [], site: [], constant: false }, + date: { + fields: ["date", "datetime"], + entry: ["publishedAt", "updatedAt"], + site: [], + constant: false, + }, + url: { fields: ["url"], entry: ["url"], site: ["url"], constant: false }, + image: { fields: ["image"], entry: ["coverImage"], site: ["logo"], constant: false }, + currency: { fields: ["text"], entry: [], site: [], constant: true }, + enum: { fields: ["select"], entry: [], site: [], constant: true }, + boolean: { fields: ["boolean"], entry: [], site: [], constant: false }, + duration: { fields: ["number"], entry: [], site: [], constant: false }, + lines: { fields: ["text"], entry: [], site: [], constant: false }, + steps: { fields: ["richText", "text"], entry: [], site: [], constant: false }, +}; + +/** One vocabulary entry: the option value (a slug, as select options are) and what it emits. */ +export interface VocabularyEntry { + readonly key: string; + readonly value: string; +} + +const SCHEMA = "https://schema.org/"; +const schemaOrg = (pairs: Record): VocabularyEntry[] => + Object.entries(pairs).map(([key, v]) => ({ key, value: `${SCHEMA}${v}` })); +const tokens = (pairs: Record): VocabularyEntry[] => + Object.entries(pairs).map(([key, value]) => ({ key, value })); + +export const VOCABULARIES = { + eventStatus: schemaOrg({ + scheduled: "EventScheduled", + cancelled: "EventCancelled", + postponed: "EventPostponed", + rescheduled: "EventRescheduled", + "moved-online": "EventMovedOnline", + }), + eventAttendanceMode: schemaOrg({ + offline: "OfflineEventAttendanceMode", + online: "OnlineEventAttendanceMode", + mixed: "MixedEventAttendanceMode", + }), + availability: schemaOrg({ + "in-stock": "InStock", + "out-of-stock": "OutOfStock", + "sold-out": "SoldOut", + "pre-order": "PreOrder", + "pre-sale": "PreSale", + "back-order": "BackOrder", + "limited-availability": "LimitedAvailability", + "online-only": "OnlineOnly", + "in-store-only": "InStoreOnly", + discontinued: "Discontinued", + }), + employmentType: tokens({ + "full-time": "FULL_TIME", + "part-time": "PART_TIME", + contractor: "CONTRACTOR", + temporary: "TEMPORARY", + intern: "INTERN", + volunteer: "VOLUNTEER", + "per-diem": "PER_DIEM", + other: "OTHER", + }), + salaryUnit: tokens({ hour: "HOUR", day: "DAY", week: "WEEK", month: "MONTH", year: "YEAR" }), + /** A `boolean` field also fits: `true` → `TELECOMMUTE`. */ + jobLocationType: tokens({ telecommute: "TELECOMMUTE" }), +} as const satisfies Record; + +export type VocabularyName = keyof typeof VOCABULARIES; + +/** + * The entry for a select option or a constant: its key, the emitted value, or (for schema.org + * URLs) the value's last segment (`cancelled`, `https://schema.org/EventCancelled` or + * `EventCancelled`). + */ +export function vocabularyEntry(name: VocabularyName, raw: string): VocabularyEntry | undefined { + return VOCABULARIES[name].find( + (e) => e.key === raw || e.value === raw || e.value === `${SCHEMA}${raw}`, + ); +} + +const entry = (property: EntryValueProperty): StructuredDataSource => ({ kind: "entry", property }); +const site = (property: SiteValueProperty): StructuredDataSource => ({ kind: "site", property }); + +const ADDRESS = [ + "streetAddress", + "addressLocality", + "postalCode", + "addressRegion", + "addressCountry", +] as const; +const address = (prefix: string, group: string): Record => + Object.fromEntries(ADDRESS.map((p) => [`${prefix}.${p}`, { kind: "text", group }])); + +const EVENT: PresetSpec = { + paths: { + name: { kind: "text", required: true, default: entry("title") }, + startDate: { kind: "date", required: true }, + "location.name": { kind: "text", required: true }, + ...address("location.address", "location.address"), + endDate: { kind: "date" }, + description: { kind: "longText", default: entry("summary") }, + image: { kind: "image", default: entry("coverImage") }, + eventStatus: { kind: "enum", vocabulary: "eventStatus" }, + eventAttendanceMode: { kind: "enum", vocabulary: "eventAttendanceMode" }, + "organizer.name": { kind: "text" }, + "organizer.url": { kind: "url" }, + "offers.price": { kind: "number" }, + "offers.priceCurrency": { kind: "currency" }, + "offers.url": { kind: "url" }, + "offers.availability": { kind: "enum", vocabulary: "availability" }, + "offers.validFrom": { kind: "date" }, + "performer.name": { kind: "text" }, + previousStartDate: { kind: "date" }, + }, + types: { + location: "Place", + "location.address": "PostalAddress", + organizer: "Organization", + offers: "Offer", + performer: "PerformingGroup", + }, +}; + +const JOB_POSTING: PresetSpec = { + paths: { + title: { kind: "text", required: true, default: entry("title") }, + description: { kind: "longText", required: true, default: entry("body") }, + datePosted: { kind: "date", required: true, default: entry("publishedAt") }, + "hiringOrganization.name": { kind: "text", required: true, default: site("name") }, + ...address("jobLocation.address", "jobLocation.address"), + validThrough: { kind: "date" }, + employmentType: { kind: "enum", vocabulary: "employmentType", many: true }, + "hiringOrganization.sameAs": { kind: "url" }, + "hiringOrganization.logo": { kind: "image", default: site("logo") }, + jobLocationType: { kind: "enum", vocabulary: "jobLocationType", alsoFields: ["boolean"] }, + "applicantLocationRequirements.name": { kind: "text" }, + "baseSalary.value": { kind: "number", target: "baseSalary.value.value" }, + "baseSalary.currency": { kind: "currency" }, + "baseSalary.unitText": { + kind: "enum", + vocabulary: "salaryUnit", + target: "baseSalary.value.unitText", + }, + directApply: { kind: "boolean" }, + identifier: { kind: "text", target: "identifier.value" }, + }, + types: { + hiringOrganization: "Organization", + jobLocation: "Place", + "jobLocation.address": "PostalAddress", + applicantLocationRequirements: "Country", + baseSalary: "MonetaryAmount", + "baseSalary.value": "QuantitativeValue", + identifier: "PropertyValue", + }, +}; + +const PRODUCT: PresetSpec = { + paths: { + name: { kind: "text", required: true, default: entry("title") }, + "offers.price": { kind: "number", required: true }, + "offers.priceCurrency": { kind: "currency" }, + "offers.availability": { kind: "enum", vocabulary: "availability" }, + "offers.priceValidUntil": { kind: "date" }, + image: { kind: "image", default: entry("coverImage") }, + description: { kind: "longText", default: entry("summary") }, + sku: { kind: "text" }, + "brand.name": { kind: "text" }, + gtin: { kind: "text" }, + }, + types: { offers: "Offer", brand: "Brand" }, +}; + +const RECIPE: PresetSpec = { + paths: { + name: { kind: "text", required: true, default: entry("title") }, + image: { kind: "image", required: true, default: entry("coverImage") }, + description: { kind: "longText" }, + recipeIngredient: { kind: "lines" }, + recipeInstructions: { kind: "steps" }, + prepTime: { kind: "duration" }, + cookTime: { kind: "duration" }, + totalTime: { kind: "duration" }, + recipeYield: { kind: "text", alsoFields: ["number"] }, + recipeCategory: { kind: "text" }, + recipeCuisine: { kind: "text" }, + keywords: { kind: "text" }, + "nutrition.calories": { kind: "text" }, + datePublished: { kind: "date" }, + }, + types: { nutrition: "NutritionInformation" }, +}; + +/** The closed table, one spec per offered type. */ +export const PRESETS: Readonly> = { + Event: EVENT, + JobPosting: JOB_POSTING, + Product: PRODUCT, + Recipe: RECIPE, +}; + +/** The offered types, in the contract's order. */ +export const PRESET_TYPES: readonly PresetType[] = ["Event", "JobPosting", "Product", "Recipe"]; + +/** Required paths of a type, then its "at least one of" groups. */ +export function requiredPaths(type: PresetType): { paths: string[]; groups: string[][] } { + const groups = new Map(); + const paths: string[] = []; + for (const [path, spec] of Object.entries(PRESETS[type].paths)) { + if (spec.required) paths.push(path); + if (spec.group) groups.set(spec.group, [...(groups.get(spec.group) ?? []), path]); + } + return { paths, groups: [...groups.values()] }; +} + +/** + * JobPosting: the address group is required unless the job is remote, i.e. `jobLocationType` and + * `applicantLocationRequirements.name` are both mapped (Google's remote-job rule). + */ +export const REMOTE_JOB_PATHS = ["jobLocationType", "applicantLocationRequirements.name"] as const; + +/** RFC-0042 §2: the serialized cap of one document. */ +export const STRUCTURED_DATA_MAX_BYTES = 32 * 1024; diff --git a/packages/core-ts/src/structured-data/values.ts b/packages/core-ts/src/structured-data/values.ts new file mode 100644 index 00000000..f5625ff3 --- /dev/null +++ b/packages/core-ts/src/structured-data/values.ts @@ -0,0 +1,268 @@ +/** + * RFC-0042 §2 "Value conversion": one fixed conversion per source and field type. Values are + * built as JSON values from typed data, never from strings of JSON: rich text becomes plain text, + * URLs must be absolute http(s), images resolve through the caller's asset lookup, and select + * options map through the path's vocabulary (an option outside it emits nothing). + */ +import type { + ContentEntry, + FieldDefinition, + ImageRef, + StructuredDataSource, +} from "@cloudhal/core-contracts"; +import { markdownToPlain, offsetMs } from "../fields/conversions.ts"; +import type { SiteFields } from "../fields/types.ts"; +import { isCalendarDay } from "../fields/value-checks.ts"; +import { type PathSpec, vocabularyEntry } from "./table.ts"; + +export type JsonLdValue = string | number | boolean | JsonLdValue[] | { [k: string]: JsonLdValue }; + +/** What the builder reads of a site. */ +export interface StructuredDataSite extends SiteFields { + readonly name: string; + readonly brand?: { readonly logo?: ImageRef | undefined } | undefined; +} + +/** What the builder reads of an entry (the entry itself, not its translation group's original). */ +export type StructuredDataEntry = Pick & + Partial>; + +/** What only the caller knows: where the site and the entry live, and where assets are served. */ +export interface StructuredDataContext { + /** The site's public origin (`https://example.com`); relative URLs resolve against it. */ + readonly origin: string; + /** The entry's public URL (absolute, or a path on `origin`). */ + readonly entryUrl?: string | undefined; + /** An image's absolute URL: an image field's asset id, a cover image or the site logo. */ + readonly imageUrl?: + | ((image: { assetId?: string; src?: string }) => string | undefined) + | undefined; +} + +/** One value source and the path it fills. */ +export interface ConvertInput { + readonly spec: PathSpec; + readonly site: StructuredDataSite; + readonly entry: StructuredDataEntry; + readonly ctx: StructuredDataContext; +} + +/** An absolute http(s) URL, or `undefined` for anything else (`javascript:`, `data:`, `mailto:`). */ +export function absoluteHttpUrl(href: string, origin: string): string | undefined { + try { + const url = new URL(href, origin); + return url.protocol === "https:" || url.protocol === "http:" ? url.href : undefined; + } catch { + return undefined; + } +} + +const pad = (n: number, w = 2) => String(Math.abs(n)).padStart(w, "0"); + +/** An instant shown with `zone`'s offset at that instant (`2026-11-05T19:30:00+03:00`). */ +export function withOffset(at: string, zone: string): string | undefined { + const ms = Date.parse(at); + if (!Number.isFinite(ms)) return undefined; + let offset: number; + try { + offset = Math.round(offsetMs(zone, ms) / 60_000); + } catch { + offset = 0; + } + const d = new Date(Math.floor(ms / 1000) * 1000 + offset * 60_000); + const date = `${pad(d.getUTCFullYear(), 4)}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`; + const time = `${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`; + const sign = offset < 0 ? "-" : "+"; + return `${date}T${time}${sign}${pad(Math.trunc(offset / 60))}:${pad(offset % 60)}`; +} + +const ENTITIES: Record = { + amp: "&", + lt: "<", + gt: ">", + quot: '"', + apos: "'", + "#39": "'", + nbsp: " ", +}; + +/** The words of an entry body: Markdown through `markdownToPlain`, HTML without its tags. */ +export function bodyText(body: ContentEntry["body"]): string { + if (body.format === "markdown") return markdownToPlain(body.value, true); + return body.value + .replace(/<(script|style)\b[\s\S]*?<\/\1\s*>/gi, " ") + .replace(/<\/(p|div|li|h[1-6]|br)\s*>|/gi, "\n") + .replace(/<[^>]*>/g, " ") + .replace(/&(#39|[a-z]+);/gi, (m, name: string) => ENTITIES[name.toLowerCase()] ?? m) + .split("\n") + .map((l) => l.replace(/\s+/g, " ").trim()) + .filter(Boolean) + .join("\n"); +} + +const nonEmpty = (s: string | undefined): string | undefined => (s?.trim() ? s.trim() : undefined); + +/** Markdown paragraphs: blank lines and list items start a new one; soft breaks join. */ +function paragraphs(markdown: string): string[] { + const out: string[] = []; + let current: string[] = []; + const flush = () => { + const text = markdownToPlain(current.join("\n"), false); + if (text) out.push(text); + current = []; + }; + for (const line of markdown.split(/\r?\n/)) { + if (!line.trim()) flush(); + else { + if (/^\s*(?:[-*+]|\d+[.)])\s+/.test(line)) flush(); + current.push(line); + } + } + flush(); + return out; +} + +const lines = (text: string): string[] => + text + .split(/\r?\n/) + .map((l) => l.trim()) + .filter(Boolean); + +const steps = (texts: string[]): JsonLdValue | undefined => + texts.length > 0 ? texts.map((text) => ({ "@type": "HowToStep", text })) : undefined; + +function fromVocabulary(spec: PathSpec, raw: unknown): JsonLdValue | undefined { + if (!spec.vocabulary) return undefined; + const one = (v: unknown) => + typeof v === "string" && spec.vocabulary + ? vocabularyEntry(spec.vocabulary, v)?.value + : undefined; + if (!Array.isArray(raw)) return one(raw); + const all = raw.map(one).filter((v): v is string => v !== undefined); + return all.length > 0 ? all : undefined; +} + +type FieldStep = ( + value: unknown, + def: FieldDefinition, + input: ConvertInput, +) => JsonLdValue | undefined; + +const str = (v: unknown): string | undefined => (typeof v === "string" ? nonEmpty(v) : undefined); + +/** One conversion per field type; the path's kind decides inside it. */ +const FIELD: Record = { + text(value, _def, { spec }) { + const s = str(value); + if (s === undefined) return undefined; + if (spec.kind === "lines") return lines(s).length > 0 ? lines(s) : undefined; + if (spec.kind === "steps") return steps(lines(s)); + if (spec.kind === "currency") return /^[A-Za-z]{3}$/.test(s) ? s.toUpperCase() : undefined; + return s; + }, + richText(value, _def, { spec }) { + const s = str(value); + if (s === undefined) return undefined; + if (spec.kind === "steps") return steps(paragraphs(s)); + return nonEmpty(markdownToPlain(s, true)); + }, + number(value, _def, { spec }) { + if (typeof value !== "number" || !Number.isFinite(value)) return undefined; + if (spec.kind === "duration") + return Number.isInteger(value) && value >= 0 ? `PT${value}M` : undefined; + if (spec.kind === "text") return String(value); + return value; + }, + boolean(value, _def, { spec }) { + if (spec.kind === "boolean") return value === true; + if (spec.vocabulary === "jobLocationType") return value === true ? "TELECOMMUTE" : undefined; + return undefined; + }, + date(value) { + return typeof value === "string" && isCalendarDay(value) ? value : undefined; + }, + datetime(value) { + const v = value as { at?: unknown; timeZone?: unknown } | null; + if (!v || typeof v.at !== "string") return undefined; + return withOffset(v.at, typeof v.timeZone === "string" ? v.timeZone : "UTC"); + }, + url(value, _def, { ctx }) { + const s = str(value); + return s === undefined ? undefined : absoluteHttpUrl(s, ctx.origin); + }, + image(value, _def, { ctx }) { + const id = str(value); + return id === undefined ? undefined : imageUrl({ assetId: id }, ctx); + }, + select(value, _def, { spec }) { + return fromVocabulary(spec, value); + }, +}; + +function imageUrl(image: { assetId?: string; src?: string }, ctx: StructuredDataContext) { + const resolved = ctx.imageUrl?.(image) ?? image.src; + return resolved === undefined ? undefined : absoluteHttpUrl(resolved, ctx.origin); +} + +function imageRef(ref: ImageRef | undefined, ctx: StructuredDataContext) { + if (!ref) return undefined; + return imageUrl({ ...(ref.assetId ? { assetId: ref.assetId } : {}), src: ref.src }, ctx); +} + +function fromEntry(property: string, { spec, entry, ctx }: ConvertInput): JsonLdValue | undefined { + switch (property) { + case "title": + return nonEmpty(entry.title); + case "summary": + return nonEmpty(entry.summary); + case "body": + return nonEmpty(bodyText(entry.body)); + case "coverImage": + return imageRef(entry.coverImage, ctx); + case "url": + return ctx.entryUrl === undefined ? undefined : absoluteHttpUrl(ctx.entryUrl, ctx.origin); + case "publishedAt": + case "updatedAt": + return spec.kind === "date" ? entry[property] : undefined; + default: + return undefined; + } +} + +function fromSite(property: string, { site, ctx }: ConvertInput): JsonLdValue | undefined { + if (property === "name") return nonEmpty(site.name); + if (property === "url") return absoluteHttpUrl("/", ctx.origin); + if (property === "logo") return imageRef(site.brand?.logo, ctx); + return undefined; +} + +function fromConstant(value: string, spec: PathSpec): JsonLdValue | undefined { + if (spec.vocabulary) return fromVocabulary(spec, value); + if (spec.kind === "currency") return /^[A-Z]{3}$/.test(value) ? value : undefined; + return nonEmpty(value); +} + +/** + * The JSON-LD value of one source for one path, or `undefined` (no value, a value that does not + * convert, an unsafe URL, an option outside the vocabulary). `field` is the resolved field. + */ +export function sourceValue( + source: StructuredDataSource, + field: { def: FieldDefinition; value: unknown } | undefined, + input: ConvertInput, +): JsonLdValue | undefined { + switch (source.kind) { + case "field": + return field && field.value !== undefined + ? FIELD[field.def.type](field.value, field.def, input) + : undefined; + case "entry": + return fromEntry(source.property, input); + case "site": + return fromSite(source.property, input); + case "text": + return fromConstant(source.value, input.spec); + default: + return undefined; + } +} diff --git a/packages/core-ts/test/fields-gate.test.ts b/packages/core-ts/test/fields-gate.test.ts new file mode 100644 index 00000000..1712e130 --- /dev/null +++ b/packages/core-ts/test/fields-gate.test.ts @@ -0,0 +1,233 @@ +/** + * RFC-0042 §3.1: `fieldGateInstant` — a datetime ends at its `at`, a date at the start of the next + * day in the site's zone, plus the grace. The date rule is checked against `temporal-polyfill` + * (the oracle; the site Worker itself never loads it) on every day of several years, in zones with + * DST at midnight, half-hour and 30-minute DST, a skipped calendar day and fixed offsets: the + * result equals Temporal's `disambiguation: "later"` and is never before the day has ended. + */ +import type { + CollectionExpiry, + CollectionSettings, + FieldDefinition, +} from "@cloudhal/core-contracts"; +import { Temporal } from "temporal-polyfill/implementation"; +import { describe, expect, it } from "vitest"; +import { dayIn } from "../src/fields/conversions.ts"; +import { + dayStartIn, + effectiveUntil, + expiryInstant, + fieldGateInstant, + nextDay, +} from "../src/fields/gate.ts"; +import { resolveFields } from "../src/fields/resolve.ts"; + +const DATE = { type: "date" } as const; +const DATETIME = { type: "datetime" } as const; +const ms = (iso: string) => Date.parse(iso); + +const ZONES = [ + "UTC", + "Europe/Istanbul", + "Europe/London", + "America/New_York", + "America/Havana", // DST at midnight (gap and overlap at 00:00) + "America/Santiago", // DST at midnight + "America/Asuncion", + "America/Sao_Paulo", // midnight DST until 2019 + "Asia/Beirut", // midnight DST + "Asia/Tehran", // midnight DST until 2022 + "Australia/Lord_Howe", // 30-minute DST + "America/St_Johns", // half-hour offset + "Asia/Kolkata", + "Africa/Casablanca", // DST suspended during Ramadan + "Pacific/Apia", // skipped 30 December 2011 + "Pacific/Kiritimati", // +14 + "Etc/GMT+12", // −12 +]; +const YEARS = [2011, 2016, 2018, 2025]; +/** + * Where the oracle itself is wrong: temporal-polyfill's transition search misses Casablanca's + * 35-day Ramadan suspension of 2018 (it reports +01:00 on 14 May; `Intl`, from the same tz data, + * says +00:00). Checked against `Intl` directly below. + */ +const ORACLE_BLIND = new Set(["Africa/Casablanca 2018"]); + +function* days(year: number): Generator { + for (let d = new Date(Date.UTC(year, 0, 1)); d.getUTCFullYear() === year; ) { + yield d.toISOString().slice(0, 10); + d = new Date(d.getTime() + 86_400_000); + } +} + +/** The oracle: 00:00 of the next day, `disambiguation: "later"`. */ +const oracle = (day: string, zone: string) => + Temporal.PlainDateTime.from(`${nextDay(day)}T00:00`).toZonedDateTime(zone, { + disambiguation: "later", + }).epochMilliseconds; + +describe("fieldGateInstant: date fields against Temporal (oracle)", () => { + for (const zone of ZONES) + it(`${zone}: every day of ${YEARS.join(", ")}`, () => { + const site = { timeZone: zone }; + for (const year of YEARS) + for (const day of days(year)) { + const got = fieldGateInstant(day, DATE, site); + if (!ORACLE_BLIND.has(`${zone} ${year}`)) + expect(got, `${zone} ${day}`).toBe(oracle(day, zone)); + const end = got as number; + // never early: at the instant, the zone's wall clock shows a later day + expect(dayIn(new Date(end).toISOString(), zone) > day, `${zone} ${day}`).toBe(true); + // and never before Temporal's own start of the next day + const start = Temporal.PlainDate.from(nextDay(day)).toZonedDateTime(zone); + expect(end >= start.epochMilliseconds, `${zone} ${day}`).toBe(true); + } + }); + + it("an ambiguous midnight takes the later instant (Havana, 2016-11-06 falls back at 01:00)", () => { + // 00:00–00:59 on 6 November happens twice: first at UTC−4, then at UTC−5 + expect(dayStartIn("2016-11-06", "America/Havana")).toBe(ms("2016-11-06T05:00:00Z")); + expect(fieldGateInstant("2016-11-05", DATE, { timeZone: "America/Havana" })).toBe( + ms("2016-11-06T05:00:00Z"), + ); + }); + + it("Casablanca's 2018 Ramadan suspension, where the oracle is blind, follows Intl", () => { + // DST ends 2018-05-13 03:00 → 02:00 (UTC+0) and resumes 2018-06-17 02:00 → 03:00 + const zone = { timeZone: "Africa/Casablanca" }; + expect(fieldGateInstant("2018-05-13", DATE, zone)).toBe(ms("2018-05-14T00:00:00Z")); + expect(fieldGateInstant("2018-06-17", DATE, zone)).toBe(ms("2018-06-17T23:00:00Z")); + expect(fieldGateInstant("2018-05-12", DATE, zone)).toBe(ms("2018-05-12T23:00:00Z")); + }); + + it("a midnight in a gap is the end of the gap (Havana, 2016-03-13 springs forward at 00:00)", () => { + expect(dayStartIn("2016-03-13", "America/Havana")).toBe(ms("2016-03-13T05:00:00Z")); + }); + + it("a skipped calendar day (Apia, 2011-12-30) ends the day before at the jump", () => { + expect(fieldGateInstant("2011-12-29", DATE, { timeZone: "Pacific/Apia" })).toBe( + ms("2011-12-30T10:00:00Z"), + ); + }); + + it("an event on 5 November is listed all of 5 November in the site's zone", () => { + expect(fieldGateInstant("2026-11-05", DATE, { timeZone: "Europe/Istanbul" })).toBe( + ms("2026-11-05T21:00:00Z"), + ); + expect(fieldGateInstant("2026-11-05", DATE, {})).toBe(ms("2026-11-06T00:00:00Z")); + expect(fieldGateInstant("2026-12-31", DATE, {})).toBe(ms("2027-01-01T00:00:00Z")); + expect(fieldGateInstant("2028-02-28", DATE, {})).toBe(ms("2028-02-29T00:00:00Z")); + }); + + it("an unknown zone takes the latest start of that day anywhere (UTC−12), never early", () => { + expect(fieldGateInstant("2026-11-05", DATE, { timeZone: "Mars/Olympus_Mons" })).toBe( + ms("2026-11-06T12:00:00Z"), + ); + }); +}); + +describe("fieldGateInstant: datetime, grace and non-values", () => { + const value = { at: "2026-11-05T16:30:00Z", timeZone: "Europe/Istanbul" }; + + it("a datetime ends at its at, whatever the site's zone", () => { + expect(fieldGateInstant(value, DATETIME, { timeZone: "America/New_York" })).toBe( + ms("2026-11-05T16:30:00Z"), + ); + }); + + it("adds afterMinutes to either", () => { + expect(fieldGateInstant(value, DATETIME, {}, 60)).toBe(ms("2026-11-05T17:30:00Z")); + expect(fieldGateInstant("2026-11-05", DATE, {}, 10_080)).toBe(ms("2026-11-13T00:00:00Z")); + expect(fieldGateInstant(value, DATETIME, {}, 0)).toBe(ms("2026-11-05T16:30:00Z")); + expect(fieldGateInstant(value, DATETIME, {}, -5)).toBe(ms("2026-11-05T16:30:00Z")); + }); + + it("no value, an unreadable value or another type: no gate", () => { + expect(fieldGateInstant(undefined, DATE, {})).toBeUndefined(); + expect(fieldGateInstant("2026-02-30", DATE, {})).toBeUndefined(); + expect(fieldGateInstant("5 Nov", DATE, {})).toBeUndefined(); + expect(fieldGateInstant({ at: "soon" }, DATETIME, {})).toBeUndefined(); + expect(fieldGateInstant("2026-11-05", DATETIME, {})).toBeUndefined(); + expect(fieldGateInstant("2026-11-05", { type: "text" }, {})).toBeUndefined(); + }); + + it("nextDay crosses months, years and leap days", () => { + expect(nextDay("2026-01-31")).toBe("2026-02-01"); + expect(nextDay("2026-12-31")).toBe("2027-01-01"); + expect(nextDay("2024-02-28")).toBe("2024-02-29"); + expect(nextDay("2023-02-28")).toBe("2023-03-01"); + }); +}); + +describe("expiryInstant and effectiveUntil", () => { + const fields: FieldDefinition[] = [ + { key: "endsAt", type: "datetime", labels: { en: { label: "Ends" } }, localized: false }, + { key: "day", type: "date", labels: { en: { label: "Day" } }, localized: false }, + { key: "venue", type: "text", labels: { en: { label: "Venue" } }, localized: true }, + ]; + const site = (expiry?: CollectionExpiry) => ({ + timeZone: "Europe/Istanbul", + collections: { events: { fields, ...(expiry ? { expiry } : {}) } as CollectionSettings }, + }); + const entry = (values: Record) => ({ + id: "entry_01HZX8K3Q9AA", + collection: "events", + fields: values, + }); + + it("reads the rule's field from the resolved values, with the grace", () => { + const s = site({ field: "endsAt", action: "unlist", afterMinutes: 60 }); + const resolved = resolveFields( + s, + entry({ endsAt: { at: "2026-11-05T20:00:00Z", timeZone: "UTC" } }), + ); + expect(expiryInstant(s, "events", resolved)).toBe(ms("2026-11-05T21:00:00Z")); + const d = site({ field: "day", action: "hide" }); + expect(expiryInstant(d, "events", resolveFields(d, entry({ day: "2026-11-05" })))).toBe( + ms("2026-11-05T21:00:00Z"), + ); + }); + + it("a translation reads the original's non-localized date", () => { + const s = site({ field: "day", action: "unlist" }); + const translation = { + ...entry({}), + id: "entry_01HZX8K3Q9TR", + translation: { group: "entry_01HZX8K3Q9AA", method: "human" as const }, + }; + const resolved = resolveFields(s, translation, { + id: "entry_01HZX8K3Q9AA", + fields: { day: "2026-11-05" }, + }); + expect(expiryInstant(s, "events", resolved)).toBe(ms("2026-11-05T21:00:00Z")); + }); + + it("no rule, no value, an unknown field or a non-date field: no derived gate", () => { + expect( + expiryInstant(site(), "events", resolveFields(site(), entry({ day: "2026-11-05" }))), + ).toBeUndefined(); + const s = site({ field: "day", action: "unlist" }); + expect(expiryInstant(s, "events", resolveFields(s, entry({})))).toBeUndefined(); + const ghost = site({ field: "gone", action: "unlist" }); + expect(expiryInstant(ghost, "events", resolveFields(ghost, entry({})))).toBeUndefined(); + const text = site({ field: "venue", action: "unlist" }); + expect( + expiryInstant(text, "events", resolveFields(text, entry({ venue: "Hall" }))), + ).toBeUndefined(); + expect(expiryInstant(s, "posts", {})).toBeUndefined(); + }); + + it("the effective gate is the earlier of availability.until and the derived one", () => { + const until = { until: "2026-11-05T12:00:00Z" }; + expect(effectiveUntil(until, ms("2026-11-06T00:00:00Z"))).toBe(ms("2026-11-05T12:00:00Z")); + expect(effectiveUntil(until, ms("2026-11-05T00:00:00Z"))).toBe(ms("2026-11-05T00:00:00Z")); + expect(effectiveUntil(until, undefined)).toBe(ms("2026-11-05T12:00:00Z")); + expect(effectiveUntil(undefined, 5)).toBe(5); + expect(effectiveUntil({ from: "2026-01-01T00:00:00Z" }, 5)).toBe(5); + expect(effectiveUntil(undefined, undefined)).toBeUndefined(); + }); + + it("an unparsable until stays closed (NaN), as gateState fails closed", () => { + expect(effectiveUntil({ until: "never" }, 5)).toBeNaN(); + }); +}); diff --git a/packages/core-ts/test/fields-query.test.ts b/packages/core-ts/test/fields-query.test.ts new file mode 100644 index 00000000..d210c9a2 --- /dev/null +++ b/packages/core-ts/test/fields-query.test.ts @@ -0,0 +1,347 @@ +/** + * RFC-0042 §4: the shared listing evaluator — operators per field type, values read by the field's + * type, `now` as the only relative value, list fields only, missing values sorting last. + */ +import type { CollectionSettings, FieldDefinition } from "@cloudhal/core-contracts"; +import { describe, expect, it } from "vitest"; +import { + canOrderBy, + checkFieldQuery, + compareByField, + FIELD_QUERY_OPS, + type FieldQuery, + matchesFieldQuery, + type ParsedFieldQuery, + parseFieldQuery, +} from "../src/fields/query.ts"; +import { type ResolvedFields, resolveFields } from "../src/fields/resolve.ts"; +import { FIELD_TYPES } from "../src/fields/types.ts"; + +const L = { en: { label: "x" } }; +const DEFS: FieldDefinition[] = [ + { key: "price", type: "number", labels: L, localized: false }, + { key: "day", type: "date", labels: L, localized: false }, + { key: "startsAt", type: "datetime", labels: L, localized: false }, + { key: "online", type: "boolean", labels: L, localized: false }, + { + key: "status", + type: "select", + labels: L, + localized: false, + options: [ + { value: "scheduled", labels: { en: "Scheduled" } }, + { value: "cancelled", labels: { en: "Cancelled" } }, + ], + }, + { + key: "tags", + type: "select", + labels: L, + localized: false, + multiple: true, + options: [ + { value: "music", labels: { en: "Music" } }, + { value: "talk", labels: { en: "Talk" } }, + { value: "food", labels: { en: "Food" } }, + ], + }, + { key: "venue", type: "text", labels: L, localized: true }, + { key: "link", type: "url", labels: L, localized: false }, + { key: "notes", type: "richText", labels: L, localized: false }, + { key: "poster", type: "image", labels: L, localized: false }, + { key: "hidden", type: "number", labels: L, localized: false }, +]; +const ALL = DEFS.map((d) => d.key).filter((k) => k !== "hidden"); + +/** A site whose `events` collection lists `listFields` (the cap of 4 is the schema's, not ours). */ +const site = (listFields: string[] = ALL, timeZone = "Europe/Istanbul") => ({ + timeZone, + collections: { events: { fields: DEFS, listFields } as CollectionSettings }, +}); + +const parse = (query: FieldQuery, listFields?: string[], zone?: string): ParsedFieldQuery => { + const r = parseFieldQuery(site(listFields, zone), "events", query); + if (!r.ok) throw new Error(JSON.stringify(r.issues)); + return r.query; +}; +const resolved = (values: Record): ResolvedFields => + resolveFields(site(), { id: "entry_01HZX8K3Q9AA", collection: "events", fields: values }); +const NOW = Date.parse("2026-11-05T21:30:00Z"); // 6 November, 00:30 in Istanbul +const matches = (query: FieldQuery, values: Record, now = NOW) => + matchesFieldQuery(resolved(values), parse(query), now); +const issues = (query: FieldQuery, listFields: string[] = ALL) => + checkFieldQuery(DEFS, listFields, query).map((i) => `${i.path}: ${i.message}`); + +describe("the operator table (§4)", () => { + it("covers every field type, and only number, date and datetime sort", () => { + expect(Object.keys(FIELD_QUERY_OPS).sort()).toEqual([...FIELD_TYPES].sort()); + expect(FIELD_TYPES.filter(canOrderBy)).toEqual(["number", "date", "datetime"]); + expect(FIELD_QUERY_OPS.boolean).toEqual(["eq"]); + expect(FIELD_QUERY_OPS.richText).toEqual(["set", "unset"]); + }); +}); + +describe("checkFieldQuery: invalid_query", () => { + it("accepts a valid query", () => { + expect( + issues({ + where: [ + { field: "startsAt", op: "gte", value: "now" }, + { field: "status", op: "in", values: ["scheduled"] }, + { field: "price", op: "lt", value: "-1.5" }, + { field: "poster", op: "set" }, + ], + orderBy: { field: "day", direction: "asc" }, + }), + ).toEqual([]); + expect(issues({})).toEqual([]); + }); + + it("refuses fields that are unknown, unlisted or being removed", () => { + expect(issues({ where: [{ field: "nope", op: "set" }] })).toEqual([ + 'where/0/field: "nope" is not a field of this collection', + ]); + expect(issues({ where: [{ field: "hidden", op: "set" }] })).toEqual([ + 'where/0/field: "hidden" is not one of the collection\'s list fields', + ]); + expect( + checkFieldQuery(DEFS, undefined, { where: [{ field: "price", op: "set" }] }), + ).toHaveLength(1); + const removing = DEFS.map((d) => + d.key === "price" ? { ...d, migration: { id: "snap_01HZX8K3Q9AB" } } : d, + ); + // a marker without from* means the field is being removed: readers treat it as gone + expect(checkFieldQuery(removing, ALL, { where: [{ field: "price", op: "set" }] })).toEqual([ + expect.objectContaining({ path: "where/0/field", code: "invalid_query" }), + ]); + }); + + it("refuses operators outside the type's set", () => { + expect(issues({ where: [{ field: "venue", op: "lt", value: "a" }] })).toEqual([ + "where/0/op: a text field takes eq, set, unset", + ]); + expect(issues({ where: [{ field: "online", op: "set" }] })).toHaveLength(1); + expect(issues({ where: [{ field: "notes", op: "eq", value: "x" }] })).toHaveLength(1); + expect(issues({ where: [{ field: "price", op: "in", values: ["1"] }] })).toHaveLength(1); + expect(issues({ where: [{ field: "price", op: "like" as "eq", value: "1" }] })).toHaveLength(1); + }); + + it("reads values by the field's type", () => { + const bad = (field: string, value: string) => + issues({ where: [{ field, op: "eq", value }] }).length; + expect(bad("price", "12")).toBe(0); + expect(bad("price", "1e3")).toBe(1); + expect(bad("price", "twelve")).toBe(1); + expect(bad("day", "2026-11-05")).toBe(0); + expect(bad("day", "now")).toBe(0); + expect(bad("day", "2026-02-30")).toBe(1); + expect(bad("day", "now+7d")).toBe(1); + expect(bad("startsAt", "2026-11-05T19:30:00+03:00")).toBe(0); + expect(bad("startsAt", "2026-11-05T16:30:00.5Z")).toBe(0); + expect(bad("startsAt", "now")).toBe(0); + expect(bad("startsAt", "2026-11-05")).toBe(1); + expect(bad("startsAt", "2026-11-05T16:30Z")).toBe(1); + expect(bad("startsAt", "2026-02-30T00:00:00Z")).toBe(1); + expect(bad("online", "true")).toBe(0); + expect(bad("online", "yes")).toBe(1); + expect(bad("status", "scheduled")).toBe(0); + expect(bad("status", "maybe")).toBe(1); + expect(bad("venue", "Hall 1")).toBe(0); + }); + + it("checks value and values against the operator", () => { + const one = (c: object) => issues({ where: [c as never] }); + expect(one({ field: "poster", op: "set", value: "x" })).toEqual([ + "where/0: set takes no value", + ]); + expect(one({ field: "status", op: "in", value: "scheduled" })).toHaveLength(1); + expect(one({ field: "status", op: "in" })).toHaveLength(1); + expect(one({ field: "status", op: "in", values: [] })).toHaveLength(1); + expect(one({ field: "status", op: "in", values: ["scheduled", "scheduled"] })).toHaveLength(1); + expect(one({ field: "status", op: "in", values: ["scheduled", "nope"] })).toEqual([ + "where/0/values/1: a select field compares with one of scheduled, cancelled", + ]); + expect(one({ field: "status", op: "eq", values: ["scheduled"] })).toHaveLength(1); + expect(one({ field: "price", op: "eq" })).toEqual(["where/0/value: eq needs a value"]); + expect(one({ field: "venue", op: "eq", value: "x".repeat(101) })).toHaveLength(1); + expect(one({ field: "venue", op: "eq", value: 5 })).toHaveLength(1); + }); + + it("checks orderBy and the limits", () => { + expect(issues({ orderBy: { field: "status", direction: "asc" } })).toEqual([ + "orderBy/field: a select field does not sort", + ]); + expect(issues({ orderBy: { field: "venue", direction: "asc" } })).toHaveLength(1); + expect(issues({ orderBy: { field: "price", direction: "up" as "asc" } })).toEqual([ + "orderBy/direction: asc or desc", + ]); + expect(issues({ orderBy: { field: "hidden", direction: "asc" } })).toHaveLength(1); + const five = Array.from({ length: 5 }, () => ({ field: "poster", op: "set" as const })); + expect(issues({ where: five as never })).toEqual(["where: at most 4 conditions"]); + expect(issues({ where: "x" as never })).toEqual(["where: a list of conditions"]); + }); + + it("parseFieldQuery reads the collection's definitions and list fields", () => { + const r = parseFieldQuery(site(["price"]), "events", { where: [{ field: "day", op: "set" }] }); + expect(r.ok).toBe(false); + expect(parseFieldQuery(site(), "posts", { where: [{ field: "price", op: "set" }] }).ok).toBe( + false, + ); + expect(parseFieldQuery({}, "events", {}).ok).toBe(true); + }); +}); + +describe("matchesFieldQuery", () => { + it("number: comparisons, ne and presence", () => { + expect(matches({ where: [{ field: "price", op: "lt", value: "10" }] }, { price: 5 })).toBe( + true, + ); + expect(matches({ where: [{ field: "price", op: "lt", value: "10" }] }, { price: 10 })).toBe( + false, + ); + expect(matches({ where: [{ field: "price", op: "lte", value: "10" }] }, { price: 10 })).toBe( + true, + ); + expect(matches({ where: [{ field: "price", op: "gt", value: "10" }] }, {})).toBe(false); + expect(matches({ where: [{ field: "price", op: "gte", value: "0" }] }, { price: 0 })).toBe( + true, + ); + expect(matches({ where: [{ field: "price", op: "eq", value: "2.50" }] }, { price: 2.5 })).toBe( + true, + ); + expect(matches({ where: [{ field: "price", op: "ne", value: "2.5" }] }, { price: 2.5 })).toBe( + false, + ); + // ne is "not eq": entries without a value match it + expect(matches({ where: [{ field: "price", op: "ne", value: "2.5" }] }, {})).toBe(true); + expect(matches({ where: [{ field: "price", op: "set" }] }, { price: 0 })).toBe(true); + expect(matches({ where: [{ field: "price", op: "unset" }] }, {})).toBe(true); + }); + + it("date: days compared as days, now is today in the site's zone", () => { + const upcoming = { where: [{ field: "day", op: "gte", value: "now" }] } as FieldQuery; + // 21:30 UTC on 5 November is already 6 November in Istanbul + expect(matches(upcoming, { day: "2026-11-05" })).toBe(false); + expect(matches(upcoming, { day: "2026-11-06" })).toBe(true); + const utc = parse(upcoming, ALL, "UTC"); + expect(matchesFieldQuery(resolved({ day: "2026-11-05" }), utc, NOW)).toBe(true); + expect( + matches({ where: [{ field: "day", op: "lt", value: "2026-12-01" }] }, { day: "2026-11-30" }), + ).toBe(true); + expect( + matches({ where: [{ field: "day", op: "eq", value: "now" }] }, { day: "2026-11-06" }), + ).toBe(true); + }); + + it("datetime: compares at; now is the injected instant", () => { + const q = { where: [{ field: "startsAt", op: "gte", value: "now" }] } as FieldQuery; + const v = { startsAt: { at: "2026-11-05T21:30:00Z", timeZone: "UTC" } }; + expect(matches(q, v)).toBe(true); + expect(matches(q, v, NOW + 1)).toBe(false); + const offset = { + where: [{ field: "startsAt", op: "eq", value: "2026-11-06T00:30:00+03:00" }], + } as FieldQuery; + expect(matches(offset, v)).toBe(true); + expect(matches(q, {})).toBe(false); + }); + + it("boolean: eq only, absent reads false", () => { + const q = (value: string) => ({ where: [{ field: "online", op: "eq", value }] }) as FieldQuery; + expect(matches(q("true"), { online: true })).toBe(true); + expect(matches(q("false"), { online: true })).toBe(false); + expect(matches(q("false"), {})).toBe(true); + expect(matches(q("true"), {})).toBe(false); + }); + + it("select: single and multiple (contains, contains any)", () => { + const status = (op: "eq" | "ne", value: string) => + ({ where: [{ field: "status", op, value }] }) as FieldQuery; + expect(matches(status("eq", "scheduled"), { status: "scheduled" })).toBe(true); + expect(matches(status("ne", "cancelled"), { status: "scheduled" })).toBe(true); + expect(matches(status("ne", "cancelled"), {})).toBe(true); + const inQ = { + where: [{ field: "status", op: "in", values: ["scheduled", "cancelled"] }], + } as unknown as FieldQuery; + expect(matches(inQ, { status: "cancelled" })).toBe(true); + expect(matches(inQ, {})).toBe(false); + const tags = (op: "eq" | "ne", value: string) => + ({ where: [{ field: "tags", op, value }] }) as FieldQuery; + expect(matches(tags("eq", "talk"), { tags: ["music", "talk"] })).toBe(true); + expect(matches(tags("eq", "food"), { tags: ["music", "talk"] })).toBe(false); + expect(matches(tags("ne", "food"), { tags: ["music"] })).toBe(true); + const any = { where: [{ field: "tags", op: "in", values: ["food", "talk"] }] } as FieldQuery; + expect(matches(any, { tags: ["talk"] })).toBe(true); + expect(matches(any, { tags: ["music"] })).toBe(false); + expect(matches({ where: [{ field: "tags", op: "unset" }] }, { tags: [] })).toBe(true); + }); + + it("text, url, rich text and image", () => { + expect( + matches({ where: [{ field: "venue", op: "eq", value: "Hall" }] }, { venue: "Hall" }), + ).toBe(true); + expect( + matches({ where: [{ field: "venue", op: "eq", value: "Hall" }] }, { venue: "hall" }), + ).toBe(false); + expect(matches({ where: [{ field: "link", op: "eq", value: "/x" }] }, { link: "/x" })).toBe( + true, + ); + expect(matches({ where: [{ field: "notes", op: "set" }] }, { notes: "Hi" })).toBe(true); + expect(matches({ where: [{ field: "venue", op: "unset" }] }, { venue: " " })).toBe(true); + expect( + matches({ where: [{ field: "poster", op: "set" }] }, { poster: "asset_01HZX8K3Q9PO" }), + ).toBe(true); + }); + + it("ANDs conditions and reads a translation's non-localized values from the original", () => { + const q = parse({ + where: [ + { field: "price", op: "lt", value: "10" }, + { field: "status", op: "eq", value: "scheduled" }, + ], + }); + expect(matchesFieldQuery(resolved({ price: 5, status: "scheduled" }), q, NOW)).toBe(true); + expect(matchesFieldQuery(resolved({ price: 5, status: "cancelled" }), q, NOW)).toBe(false); + const translation = resolveFields( + site(), + { + id: "entry_01HZX8K3Q9TR", + collection: "events", + fields: { price: 99 }, + translation: { group: "entry_01HZX8K3Q9AA", method: "human" as const }, + }, + { id: "entry_01HZX8K3Q9AA", fields: { price: 5, status: "scheduled" } }, + ); + expect(matchesFieldQuery(translation, q, NOW)).toBe(true); + expect(matchesFieldQuery(resolved({}), parse({}), NOW)).toBe(true); + }); +}); + +describe("compareByField", () => { + const sort = (query: FieldQuery, rows: Record[]) => { + const parsed = parse(query); + return rows + .map((values, i) => ({ i, r: resolved(values) })) + .sort((a, b) => compareByField(a.r, b.r, parsed) || a.i - b.i) + .map(({ i }) => i); + }; + const rows = [{ price: 3 }, {}, { price: 1 }, { price: 2 }, { price: 1 }]; + + it("orders numbers, values missing last in both directions, ties left to the caller", () => { + expect(sort({ orderBy: { field: "price", direction: "asc" } }, rows)).toEqual([2, 4, 3, 0, 1]); + expect(sort({ orderBy: { field: "price", direction: "desc" } }, rows)).toEqual([0, 3, 2, 4, 1]); + }); + + it("orders dates and datetimes by instant", () => { + const days = [{ day: "2026-12-01" }, { day: "2026-01-15" }, {}]; + expect(sort({ orderBy: { field: "day", direction: "asc" } }, days)).toEqual([1, 0, 2]); + const times = [ + { startsAt: { at: "2026-11-05T18:00:00Z", timeZone: "UTC" } }, + { startsAt: { at: "2026-11-05T17:00:00Z", timeZone: "Europe/Istanbul" } }, + ]; + expect(sort({ orderBy: { field: "startsAt", direction: "desc" } }, times)).toEqual([0, 1]); + }); + + it("is 0 without orderBy", () => { + expect(compareByField(resolved({ price: 1 }), resolved({ price: 2 }), parse({}))).toBe(0); + }); +}); diff --git a/packages/core-ts/test/structured-data.test.ts b/packages/core-ts/test/structured-data.test.ts new file mode 100644 index 00000000..6ddd38d4 --- /dev/null +++ b/packages/core-ts/test/structured-data.test.ts @@ -0,0 +1,601 @@ +/** + * RFC-0042 §2: the closed preset table, the save-time check and the per-entry JSON-LD builder. + */ +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import type { + CollectionSettings, + ContentEntry, + FieldDefinition, + Site, + StructuredDataPreset, +} from "@cloudhal/core-contracts"; +import { describe, expect, it } from "vitest"; +import { resolveFields } from "../src/fields/resolve.ts"; +import { + absoluteHttpUrl, + bodyText, + checkStructuredData, + KIND_SOURCES, + PRESET_TYPES, + PRESETS, + requiredPaths, + STRUCTURED_DATA_MAX_BYTES, + type StructuredDataContext, + structuredDataErrors, + structuredDataFor, + VOCABULARIES, + vocabularyEntry, + withOffset, +} from "../src/structured-data/index.ts"; + +const FIXTURES = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "../../core-contracts/fixtures/valid/site", +); +const fixture = (name: string): Site => + JSON.parse(readFileSync(path.join(FIXTURES, `${name}.json`), "utf8")) as Site; + +const L = { en: { label: "x" } }; +const f = (key: string, type: FieldDefinition["type"], extra: object = {}): FieldDefinition => + ({ key, type, labels: L, localized: false, ...extra }) as FieldDefinition; +const EVENT_FIELDS: FieldDefinition[] = [ + f("startsAt", "datetime"), + f("endsAt", "datetime"), + f("day", "date"), + f("venue", "text"), + f("city", "text"), + f("lines", "text", { multiline: true }), + f("price", "number"), + f("minutes", "number", { integer: true }), + f("remote", "boolean"), + f("status", "select", { + options: [ + { value: "scheduled", labels: { en: "S" } }, + { value: "cancelled", labels: { en: "C" } }, + { value: "sold-out", labels: { en: "X" } }, + ], + }), + f("kinds", "select", { + multiple: true, + options: [ + { value: "full-time", labels: { en: "F" } }, + { value: "part-time", labels: { en: "P" } }, + ], + }), + f("poster", "image"), + f("tickets", "url"), + f("notes", "richText"), +]; + +const siteWith = (preset: StructuredDataPreset, fields = EVENT_FIELDS) => ({ + name: "Halls & Co", + timeZone: "Europe/Istanbul", + brand: { logo: { src: "/logo.png", alt: "Logo" } }, + collections: { events: { fields, structuredData: preset } as CollectionSettings }, +}); +const codes = (preset: StructuredDataPreset, fields?: FieldDefinition[]) => + checkStructuredData(siteWith(preset, fields)).map( + (i) => + `${i.code}${i.warning ? "?" : ""} ${i.path.replace("collections/events/structuredData/", "")}`, + ); + +const EVENT: StructuredDataPreset = { + type: "Event", + properties: { + startDate: { kind: "field", field: "startsAt" }, + "location.name": { kind: "field", field: "venue" }, + "location.address.addressLocality": { kind: "field", field: "city" }, + }, +}; + +describe("the closed table", () => { + it("offers four types, each with required paths, and never ratings or reviews", () => { + expect(PRESET_TYPES).toEqual(["Event", "JobPosting", "Product", "Recipe"]); + for (const type of PRESET_TYPES) { + expect(requiredPaths(type).paths.length, type).toBeGreaterThan(0); + for (const path of Object.keys(PRESETS[type].paths)) { + expect(path).toMatch(/^[a-z][A-Za-z]{0,39}(\.[a-z][A-Za-z]{0,39}){0,2}$/); + expect(path).not.toMatch(/rating|review/i); + } + } + expect(requiredPaths("Event").paths).toEqual(["name", "startDate", "location.name"]); + expect(requiredPaths("Event").groups[0]).toHaveLength(5); + expect(requiredPaths("JobPosting").paths).toEqual([ + "title", + "description", + "datePosted", + "hiringOrganization.name", + ]); + expect(requiredPaths("Product").paths).toEqual(["name", "offers.price"]); + expect(requiredPaths("Recipe").paths).toEqual(["name", "image"]); + }); + + it("defaults point at sources that fit their paths", () => { + for (const type of PRESET_TYPES) + for (const [path, spec] of Object.entries(PRESETS[type].paths)) { + const d = spec.default; + if (!d) continue; + const sources = KIND_SOURCES[spec.kind]; + if (d.kind === "entry") expect(sources.entry, `${type} ${path}`).toContain(d.property); + if (d.kind === "site") expect(sources.site, `${type} ${path}`).toContain(d.property); + } + }); + + it("vocabularies match keys, emitted values and schema.org short names", () => { + expect(vocabularyEntry("eventStatus", "cancelled")?.value).toBe( + "https://schema.org/EventCancelled", + ); + expect(vocabularyEntry("eventStatus", "EventCancelled")?.key).toBe("cancelled"); + expect(vocabularyEntry("employmentType", "FULL_TIME")?.key).toBe("full-time"); + expect(vocabularyEntry("availability", "maybe")).toBeUndefined(); + for (const entries of Object.values(VOCABULARIES)) + for (const e of entries) expect(e.key).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*$/); + }); +}); + +describe("checkStructuredData (save time)", () => { + it("accepts the contract's valid fixtures", () => { + expect(checkStructuredData(fixture("collection-stage3"))).toEqual([]); + expect(checkStructuredData(fixture("collection-stage3-presets"))).toEqual([]); + expect(checkStructuredData({})).toEqual([]); + }); + + it("accepts a minimal Event (name from the entry's title by default)", () => { + expect(codes(EVENT)).toEqual([]); + }); + + it("refuses paths outside the type's table", () => { + const p = { + ...EVENT, + properties: { ...EVENT.properties, aggregateRating: { kind: "text", value: "5" } }, + }; + expect(codes(p as StructuredDataPreset)).toEqual(["unknown_path properties/aggregateRating"]); + const proto = { + ...EVENT, + properties: { ...EVENT.properties, toString: { kind: "text", value: "x" } }, + }; + expect(codes(proto as StructuredDataPreset)).toEqual(["unknown_path properties/toString"]); + }); + + it("refuses unknown fields and fields whose type does not fit", () => { + const p = (source: object, path = "endDate") => + codes({ + ...EVENT, + properties: { ...EVENT.properties, [path]: source }, + } as StructuredDataPreset); + expect(p({ kind: "field", field: "nope" })).toEqual(["unknown_field properties/endDate/field"]); + expect(p({ kind: "field", field: "venue" })).toEqual([ + "source_mismatch properties/endDate/field", + ]); + expect(p({ kind: "field", field: "day" })).toEqual([]); + expect(p({ kind: "field", field: "venue" }, "offers.price")).toHaveLength(1); + expect(p({ kind: "field", field: "notes" }, "description")).toEqual([]); + expect(p({ kind: "field", field: "poster" }, "image")).toEqual([]); + expect(p({ kind: "field", field: "tickets" }, "image")).toHaveLength(1); + }); + + it("refuses entry and site properties that do not fit", () => { + const p = (source: object, path: string) => + codes({ + ...EVENT, + properties: { ...EVENT.properties, [path]: source }, + } as StructuredDataPreset); + expect(p({ kind: "entry", property: "publishedAt" }, "endDate")).toEqual([]); + expect(p({ kind: "entry", property: "title" }, "endDate")).toEqual([ + "source_mismatch properties/endDate/property", + ]); + expect(p({ kind: "site", property: "url" }, "organizer.url")).toEqual([]); + expect(p({ kind: "site", property: "logo" }, "organizer.url")).toHaveLength(1); + expect(p({ kind: "entry", property: "body" }, "description")).toEqual([]); + expect(p({ kind: "entry", property: "body" }, "location.name")).toHaveLength(1); + }); + + it("checks constants: kinds that take none, vocabularies, ISO 4217, required paths", () => { + const p = (value: string, path: string) => + codes({ + ...EVENT, + properties: { ...EVENT.properties, [path]: { kind: "text", value } }, + } as StructuredDataPreset); + expect(p("EUR", "offers.priceCurrency")).toEqual([]); + expect(p("eur", "offers.priceCurrency")).toEqual([ + "currency properties/offers.priceCurrency/value", + ]); + expect(p("cancelled", "eventStatus")).toEqual([]); + expect(p("EventCancelled", "eventStatus")).toEqual([]); + expect(p("maybe", "eventStatus")).toEqual(["vocabulary properties/eventStatus/value"]); + expect(p("12", "offers.price")).toEqual(["source_mismatch properties/offers.price/value"]); + expect(p("https://x.test", "offers.url")).toHaveLength(1); + expect(p("2026-11-05", "startDate")).toEqual(["source_mismatch properties/startDate/value"]); + expect(p("Grand Hall", "location.name")).toEqual([ + "constant_required properties/location.name/value", + ]); + expect(p("Organised by us", "organizer.name")).toEqual([]); + }); + + it("warns about select options outside a vocabulary, refuses a multiple select on a single path", () => { + const p = (path: string, field: string) => + codes({ ...EVENT, properties: { ...EVENT.properties, [path]: { kind: "field", field } } }); + // sold-out is not an event status: a warning, the save goes through + const issues = p("eventStatus", "status"); + expect(issues).toEqual(["vocabulary? properties/eventStatus/field"]); + expect( + structuredDataErrors( + checkStructuredData( + siteWith({ + ...EVENT, + properties: { ...EVENT.properties, eventStatus: { kind: "field", field: "status" } }, + }), + ), + ), + ).toEqual([]); + expect(p("eventStatus", "kinds")).toEqual(["source_mismatch properties/eventStatus/field"]); + }); + + it("requires every required path, and an address group from a field", () => { + expect( + codes({ type: "Event", properties: { startDate: { kind: "field", field: "startsAt" } } }), + ).toEqual([ + "required_missing properties/location.name", + "required_missing properties/location.address", + ]); + // a constant country alone does not place an event + expect( + codes({ + type: "Event", + properties: { + ...EVENT.properties, + "location.address.addressLocality": { kind: "text", value: "Istanbul" }, + }, + }), + ).toEqual(["required_missing properties/location.address"]); + expect(codes({ type: "Product", properties: { sku: { kind: "text", value: "A1" } } })).toEqual([ + "required_missing properties/offers.price", + ]); + expect( + codes({ type: "Recipe", properties: { keywords: { kind: "text", value: "soup" } } }), + ).toEqual([]); + }); + + it("JobPosting: an address, unless the job is remote with applicant locations", () => { + expect( + codes({ type: "JobPosting", properties: { validThrough: { kind: "field", field: "day" } } }), + ).toEqual(["required_missing properties/jobLocation.address"]); + expect( + codes({ + type: "JobPosting", + properties: { + jobLocationType: { kind: "field", field: "remote" }, + "applicantLocationRequirements.name": { kind: "text", value: "Türkiye" }, + employmentType: { kind: "field", field: "kinds" }, + "baseSalary.unitText": { kind: "text", value: "MONTH" }, + }, + }), + ).toEqual([]); + }); + + it("checks lines and steps against multiline text", () => { + const fields = [...EVENT_FIELDS, f("single", "text")]; + const recipe = (path: string, field: string) => + codes({ type: "Recipe", properties: { [path]: { kind: "field", field } } }, fields); + expect(recipe("recipeIngredient", "lines")).toEqual([]); + expect(recipe("recipeIngredient", "single")).toEqual([ + "source_mismatch properties/recipeIngredient/field", + ]); + expect(recipe("recipeInstructions", "notes")).toEqual([]); + expect(recipe("recipeInstructions", "single")).toHaveLength(1); + expect(recipe("totalTime", "minutes")).toEqual([]); + expect(recipe("recipeYield", "price")).toEqual([]); + }); +}); + +const CTX: StructuredDataContext = { + origin: "https://halls.test", + entryUrl: "/events/jazz-night", + imageUrl: (img) => (img.assetId ? `/assets/${img.assetId}.webp` : img.src), +}; +const ENTRY = { + collection: "events", + title: "Jazz Night", + summary: "Live jazz.", + body: { format: "markdown", value: "# Jazz\n\nLive **jazz** in the [hall](/hall)." }, + coverImage: { src: "/cover.jpg", alt: "Cover", assetId: "asset_01HZX8K3Q9CV" }, + publishedAt: "2026-10-01T09:00:00Z", + updatedAt: "2026-10-02T09:00:00Z", +} satisfies Partial; + +const build = (preset: StructuredDataPreset, values: Record, entry = ENTRY) => { + const site = siteWith(preset); + const resolved = resolveFields(site, { + id: "entry_01HZX8K3Q9AA", + collection: "events", + fields: values, + }); + return structuredDataFor(site, resolved, entry, CTX); +}; +const EVENT_VALUES = { + startsAt: { at: "2026-11-05T16:30:00Z", timeZone: "Europe/Istanbul" }, + venue: "Grand Hall", + city: "Istanbul", +}; + +describe("structuredDataFor (per entry)", () => { + it("builds an Event with nested @types, offsets from the value's zone and defaults", () => { + const preset: StructuredDataPreset = { + type: "Event", + properties: { + ...EVENT.properties, + endDate: { kind: "field", field: "day" }, + eventStatus: { kind: "field", field: "status" }, + "location.address.addressCountry": { kind: "text", value: "TR" }, + "offers.price": { kind: "field", field: "price" }, + "offers.priceCurrency": { kind: "text", value: "TRY" }, + "offers.url": { kind: "field", field: "tickets" }, + "organizer.name": { kind: "site", property: "name" }, + "organizer.url": { kind: "site", property: "url" }, + image: { kind: "field", field: "poster" }, + }, + }; + const r = build(preset, { + ...EVENT_VALUES, + day: "2026-11-06", + status: "cancelled", + price: 250, + tickets: "/tickets", + poster: "asset_01HZX8K3Q9PO", + }); + expect(r.issues).toEqual([]); + expect(r.doc).toEqual({ + "@context": "https://schema.org", + "@type": "Event", + name: "Jazz Night", + startDate: "2026-11-05T19:30:00+03:00", + location: { + "@type": "Place", + name: "Grand Hall", + address: { "@type": "PostalAddress", addressLocality: "Istanbul", addressCountry: "TR" }, + }, + endDate: "2026-11-06", + description: "Live jazz.", + image: "https://halls.test/assets/asset_01HZX8K3Q9PO.webp", + eventStatus: "https://schema.org/EventCancelled", + organizer: { "@type": "Organization", name: "Halls & Co", url: "https://halls.test/" }, + offers: { + "@type": "Offer", + price: 250, + priceCurrency: "TRY", + url: "https://halls.test/tickets", + }, + }); + }); + + it("emits no document when a required value is missing, and says which", () => { + const r = build(EVENT, { venue: "Grand Hall", city: "Istanbul" }); + expect(r.doc).toBeUndefined(); + expect(r.issues).toEqual([ + { + path: "startDate", + code: "required_value_missing", + message: "Structured data: startDate missing", + }, + ]); + expect(build(EVENT, { ...EVENT_VALUES, city: undefined }).issues.map((i) => i.path)).toEqual([ + "location.address", + ]); + expect(build(EVENT, EVENT_VALUES, { ...ENTRY, title: " " }).issues.map((i) => i.path)).toEqual([ + "name", + ]); + }); + + it("returns nothing without a preset", () => { + const site = { name: "S", collections: { events: {} } }; + expect(structuredDataFor(site, {}, ENTRY, CTX)).toEqual({ issues: [] }); + }); + + it("drops unsafe URLs, options outside the vocabulary and fields that no longer fit", () => { + const preset: StructuredDataPreset = { + type: "Event", + properties: { + ...EVENT.properties, + "offers.url": { kind: "field", field: "tickets" }, + eventStatus: { kind: "field", field: "status" }, + endDate: { kind: "field", field: "venue" }, + }, + }; + const r = build(preset, { ...EVENT_VALUES, tickets: "mailto:x@y.test", status: "sold-out" }); + expect(r.doc).toBeDefined(); + expect(r.doc).not.toHaveProperty("offers"); + expect(r.doc).not.toHaveProperty("eventStatus"); + expect(r.doc).not.toHaveProperty("endDate"); + }); + + it("builds a remote JobPosting: body as text, site defaults, salary nesting, employment types", () => { + const preset: StructuredDataPreset = { + type: "JobPosting", + properties: { + validThrough: { kind: "field", field: "day" }, + jobLocationType: { kind: "field", field: "remote" }, + "applicantLocationRequirements.name": { kind: "text", value: "Türkiye" }, + employmentType: { kind: "field", field: "kinds" }, + "baseSalary.value": { kind: "field", field: "price" }, + "baseSalary.currency": { kind: "text", value: "EUR" }, + "baseSalary.unitText": { kind: "text", value: "month" }, + directApply: { kind: "field", field: "remote" }, + identifier: { kind: "field", field: "venue" }, + }, + }; + const values = { + day: "2026-12-01", + remote: true, + kinds: ["full-time", "part-time"], + price: 4000, + venue: "J-7", + }; + const r = build(preset, values); + expect(r.issues).toEqual([]); + expect(r.doc).toMatchObject({ + "@type": "JobPosting", + title: "Jazz Night", + description: "Jazz\nLive jazz in the hall.", + datePosted: "2026-10-01T09:00:00Z", + hiringOrganization: { + "@type": "Organization", + name: "Halls & Co", + logo: "https://halls.test/logo.png", + }, + validThrough: "2026-12-01", + employmentType: ["FULL_TIME", "PART_TIME"], + jobLocationType: "TELECOMMUTE", + applicantLocationRequirements: { "@type": "Country", name: "Türkiye" }, + baseSalary: { + "@type": "MonetaryAmount", + currency: "EUR", + value: { "@type": "QuantitativeValue", value: 4000, unitText: "MONTH" }, + }, + directApply: true, + identifier: { "@type": "PropertyValue", value: "J-7" }, + }); + // an on-site job needs an address + expect(build(preset, { ...values, remote: false }).issues.map((i) => i.path)).toEqual([ + "jobLocation.address", + ]); + }); + + it("builds a Recipe: ingredient lines, steps per paragraph, durations", () => { + const preset: StructuredDataPreset = { + type: "Recipe", + properties: { + recipeIngredient: { kind: "field", field: "lines" }, + recipeInstructions: { kind: "field", field: "notes" }, + totalTime: { kind: "field", field: "minutes" }, + prepTime: { kind: "field", field: "price" }, + recipeYield: { kind: "field", field: "minutes" }, + datePublished: { kind: "entry", property: "publishedAt" }, + }, + }; + const r = build(preset, { + lines: "2 eggs\n\n 1 cup flour \n", + notes: "Whisk the eggs\nuntil foamy.\n\n1. Fold in **flour**.\n2. Bake.", + minutes: 45, + price: 2.5, + }); + expect(r.doc).toMatchObject({ + "@type": "Recipe", + name: "Jazz Night", + image: "https://halls.test/assets/asset_01HZX8K3Q9CV.webp", + recipeIngredient: ["2 eggs", "1 cup flour"], + recipeInstructions: [ + { "@type": "HowToStep", text: "Whisk the eggs until foamy." }, + { "@type": "HowToStep", text: "Fold in flour." }, + { "@type": "HowToStep", text: "Bake." }, + ], + totalTime: "PT45M", + recipeYield: "45", + datePublished: "2026-10-01T09:00:00Z", + }); + // a non-integer number of minutes is no duration + expect(r.doc).not.toHaveProperty("prepTime"); + // no cover image: no Recipe + const { coverImage: _, ...bare } = ENTRY; + expect(build(preset, {}, bare as typeof ENTRY).issues.map((i) => i.path)).toEqual(["image"]); + }); + + it("builds a Product, and uses the entry URL source", () => { + const preset: StructuredDataPreset = { + type: "Product", + properties: { + "offers.price": { kind: "field", field: "price" }, + "offers.priceCurrency": { kind: "field", field: "venue" }, + "offers.availability": { kind: "text", value: "in-stock" }, + "offers.priceValidUntil": { kind: "entry", property: "updatedAt" }, + }, + }; + expect(build(preset, { price: 9.99, venue: "eur" }).doc).toMatchObject({ + "@type": "Product", + offers: { + "@type": "Offer", + price: 9.99, + priceCurrency: "EUR", + availability: "https://schema.org/InStock", + priceValidUntil: "2026-10-02T09:00:00Z", + }, + }); + expect(build(preset, { price: 0 }).doc).toMatchObject({ offers: { price: 0 } }); + }); + + it("keeps a document under 32 KB: optional properties first, description last", () => { + const long = "word ".repeat(7000).trim(); // ~35 KB + const preset: StructuredDataPreset = { + type: "Event", + properties: { ...EVENT.properties, description: { kind: "field", field: "notes" } }, + }; + const r = build(preset, { ...EVENT_VALUES, notes: long }); + expect(r.doc).toBeDefined(); + // the optional image goes first, then the description; required properties stay + expect(r.doc).not.toHaveProperty("description"); + expect(r.doc).not.toHaveProperty("image"); + expect(r.doc).toMatchObject({ name: "Jazz Night", location: { name: "Grand Hall" } }); + expect(JSON.stringify(r.doc).length).toBeLessThanOrEqual(STRUCTURED_DATA_MAX_BYTES); + }); + + it("shortens a required JobPosting description to fit, multi-byte text included", () => { + const preset: StructuredDataPreset = { + type: "JobPosting", + properties: { + "jobLocation.address.addressLocality": { kind: "field", field: "city" }, + "hiringOrganization.sameAs": { kind: "site", property: "url" }, + }, + }; + const body = { format: "markdown" as const, value: "çalışma ".repeat(6000) }; + const r = build(preset, { city: "Izmir" }, { ...ENTRY, body }); + expect(r.issues).toEqual([]); + const size = new TextEncoder().encode(JSON.stringify(r.doc)).length; + expect(size).toBeLessThanOrEqual(STRUCTURED_DATA_MAX_BYTES); + expect(String(r.doc?.description).endsWith("…")).toBe(true); + expect(r.doc?.hiringOrganization).not.toHaveProperty("sameAs"); + }); + + it("refuses a document whose required properties alone exceed the cap", () => { + const r = build(EVENT, EVENT_VALUES, { ...ENTRY, title: "x".repeat(40_000) }); + expect(r.doc).toBeUndefined(); + expect(r.issues.map((i) => i.code)).toEqual(["too_large"]); + }); +}); + +describe("value helpers", () => { + it("absoluteHttpUrl keeps http(s) only", () => { + expect(absoluteHttpUrl("/a b", "https://x.test")).toBe("https://x.test/a%20b"); + expect(absoluteHttpUrl("http://y.test/", "https://x.test")).toBe("http://y.test/"); + expect(absoluteHttpUrl("javascript:alert(1)", "https://x.test")).toBeUndefined(); + expect(absoluteHttpUrl("data:text/html,x", "https://x.test")).toBeUndefined(); + expect(absoluteHttpUrl("tel:+1", "https://x.test")).toBeUndefined(); + expect(absoluteHttpUrl("/x", "not a url")).toBeUndefined(); + }); + + it("withOffset shows the instant in its zone, across DST and half-hour zones", () => { + expect(withOffset("2026-07-01T12:00:00Z", "America/New_York")).toBe( + "2026-07-01T08:00:00-04:00", + ); + expect(withOffset("2026-01-01T12:00:00Z", "America/New_York")).toBe( + "2026-01-01T07:00:00-05:00", + ); + expect(withOffset("2026-01-01T12:00:00Z", "Asia/Kolkata")).toBe("2026-01-01T17:30:00+05:30"); + expect(withOffset("2026-01-01T12:00:00Z", "America/St_Johns")).toBe( + "2026-01-01T08:30:00-03:30", + ); + expect(withOffset("2026-01-01T12:00:00Z", "UTC")).toBe("2026-01-01T12:00:00+00:00"); + expect(withOffset("2026-01-01T12:00:00Z", "Nowhere/Zone")).toBe("2026-01-01T12:00:00+00:00"); + expect(withOffset("soon", "UTC")).toBeUndefined(); + }); + + it("bodyText reads Markdown and HTML bodies as plain text", () => { + expect(bodyText({ format: "markdown", value: "## Hi\n\n- **a**\n- [b](/b)" })).toBe("Hi\na\nb"); + expect( + bodyText({ + format: "html", + value: "

Tom & Jerry

a
b c <3

", + }), + ).toBe("Tom & Jerry\na\nb c <3"); + }); +}); diff --git a/packages/core-ts/test/worker-time-free.test.ts b/packages/core-ts/test/worker-time-free.test.ts index ce1f671e..5f97df73 100644 --- a/packages/core-ts/test/worker-time-free.test.ts +++ b/packages/core-ts/test/worker-time-free.test.ts @@ -78,11 +78,12 @@ 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 and fields entries and the schedules engine reach no temporal-polyfill", async () => { + it("core-ts's index, its availability, fields and structured-data 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, "core-ts/src/structured-data/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"), From 491d6e656d13072288d584cabf6e68b5cddb2e1a Mon Sep 17 00:00:00 2001 From: Madraka <8898640+Madraka@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:11:21 +0300 Subject: [PATCH 2/3] refactor(contracts): structured-data constant source kind is `constant` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `text` named the value's type while `field`, `entry` and `site` name its origin, and `text` is already a field type (RFC-0041). `constant` matches the other kinds, the prior art (R2RML rr:constant, JSON Schema const) and leaves room for typed constants later. Decided by Halil before release; RFC-0042 §1 and ADR-0044 updated. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com> --- crates/cloudhal-schema/src/generated/site.rs | 6 +- docs/adr/0044-collections-stage-3.md | 2 +- docs/rfcs/0042-collections-stage-3.md | 8 +- .../invalid/site/expiry-action-delete.json | 4 +- .../invalid/site/expiry-extra-property.json | 4 +- .../invalid/site/expiry-field-kebab.json | 4 +- .../invalid/site/expiry-grace-fraction.json | 4 +- .../invalid/site/expiry-grace-negative.json | 4 +- .../invalid/site/expiry-grace-too-long.json | 4 +- .../invalid/site/expiry-without-action.json | 4 +- .../invalid/site/list-fields-bad-key.json | 4 +- .../invalid/site/list-fields-duplicate.json | 4 +- .../invalid/site/list-fields-empty.json | 4 +- .../invalid/site/list-fields-too-many.json | 4 +- ...tructured-data-constant-control-char.json} | 6 +- ...on => structured-data-constant-empty.json} | 6 +- ...=> structured-data-constant-too-long.json} | 6 +- ...tructured-data-entry-unknown-property.json | 4 +- .../site/structured-data-extra-property.json | 4 +- .../site/structured-data-faq-page.json | 4 +- .../site/structured-data-field-missing.json | 4 +- .../structured-data-field-with-value.json | 4 +- .../site/structured-data-kind-html.json | 4 +- .../site/structured-data-path-digit.json | 6 +- .../site/structured-data-path-too-deep.json | 6 +- .../site/structured-data-path-upper.json | 6 +- .../structured-data-site-entry-property.json | 4 +- ...structured-data-site-unknown-property.json | 4 +- .../structured-data-too-many-properties.json | 82 +++++++++---------- .../site/structured-data-without-type.json | 4 +- .../valid/site/collection-stage3-presets.json | 6 +- .../valid/site/collection-stage3.json | 4 +- .../core-contracts/schemas/site.schema.json | 4 +- .../core-contracts/src/generated/index.ts | 2 +- .../core-contracts/src/generated/schemas.ts | 4 +- packages/core-contracts/src/generated/site.ts | 6 +- .../src/generated/validators.js | 6 +- packages/core-ts/src/structured-data/check.ts | 4 +- .../core-ts/src/structured-data/values.ts | 2 +- packages/core-ts/test/structured-data.test.ts | 28 +++---- 40 files changed, 140 insertions(+), 140 deletions(-) rename packages/core-contracts/fixtures/invalid/site/{structured-data-text-control-char.json => structured-data-constant-control-char.json} (98%) rename packages/core-contracts/fixtures/invalid/site/{structured-data-text-empty.json => structured-data-constant-empty.json} (98%) rename packages/core-contracts/fixtures/invalid/site/{structured-data-text-too-long.json => structured-data-constant-too-long.json} (98%) diff --git a/crates/cloudhal-schema/src/generated/site.rs b/crates/cloudhal-schema/src/generated/site.rs index 3c28a124..6b5f8aef 100644 --- a/crates/cloudhal-schema/src/generated/site.rs +++ b/crates/cloudhal-schema/src/generated/site.rs @@ -5390,9 +5390,9 @@ pub enum StructuredDataSource { #[doc = "SiteValueSource\n\nA property of the site."] #[serde(rename = "site")] Site { property: SiteValueProperty }, - #[doc = "TextValueSource\n\nA plain-text constant: no control characters, no markup meaning; vocabulary-checked on enum paths (in code)."] - #[serde(rename = "text")] - Text { value: StructuredDataSourceValue }, + #[doc = "ConstantValueSource\n\nA plain-text constant: no control characters, no markup meaning; vocabulary-checked on enum paths (in code)."] + #[serde(rename = "constant")] + Constant { value: StructuredDataSourceValue }, } #[doc = "`StructuredDataSourceValue`"] #[derive(:: serde :: Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] diff --git a/docs/adr/0044-collections-stage-3.md b/docs/adr/0044-collections-stage-3.md index 556caaed..e35bc504 100644 --- a/docs/adr/0044-collections-stage-3.md +++ b/docs/adr/0044-collections-stage-3.md @@ -16,7 +16,7 @@ The open stage 3 items from RFC-0040 and RFC-0041 address these: structured data 1. **Contract:** new optional collection settings. There is no `schemaVersion` bump. - `listFields`: up to 4 keys. - `expiry`: `{field, action: "unlist" | "hide", afterMinutes?}`. - - `structuredData`: `{type, properties}`. Each property names its source by `kind`: `field`, `entry`, `site` or `text` (a plain-text constant). The entry and site property names are named `$defs` (cargo-typify merges inline enums). + - `structuredData`: `{type, properties}`. Each property names its source by `kind`: `field`, `entry`, `site` or `constant` (plain text in stage 3). The entry and site property names are named `$defs` (cargo-typify merges inline enums). - `contentList` sections gain `where` (up to 4 conditions) and `orderBy`. - Which properties each type accepts, the field types that fit them and the required ones are a closed table in code (`@cloudhal/core-ts/structured-data`). 2. **Presets:** Event, JobPosting, Product and Recipe. diff --git a/docs/rfcs/0042-collections-stage-3.md b/docs/rfcs/0042-collections-stage-3.md index 929127e2..52e8ea34 100644 --- a/docs/rfcs/0042-collections-stage-3.md +++ b/docs/rfcs/0042-collections-stage-3.md @@ -76,8 +76,8 @@ All additions are optional. No `schemaVersion` bump. "properties": { "kind": { "type": "string", "enum": ["entry"] }, "property": { "$ref": "#/$defs/EntryValueProperty" } } }, { "title": "SiteValueSource", "type": "object", "additionalProperties": false, "required": ["kind", "property"], "properties": { "kind": { "type": "string", "enum": ["site"] }, "property": { "$ref": "#/$defs/SiteValueProperty" } } }, - { "title": "TextValueSource", "type": "object", "additionalProperties": false, "required": ["kind", "value"], - "properties": { "kind": { "type": "string", "enum": ["text"] }, + { "title": "ConstantValueSource", "type": "object", "additionalProperties": false, "required": ["kind", "value"], + "properties": { "kind": { "type": "string", "enum": ["constant"] }, "value": { "type": "string", "minLength": 1, "maxLength": 200, "pattern": "^[^\\x00-\\x1F\\x7F]+$" } } } ] }, @@ -111,9 +111,9 @@ All additions are optional. No `schemaVersion` bump. - **Discriminator `kind`** for sources, rule 2's default: these are not field types. - **Condition values are strings, typed by the field.** A `type: ["string","number","boolean"]` union is outside the generator-safe subset, and a `oneOf` per value type would make sections verbose for agents. RFC-0002's filter is a string read against the site's taxonomy in the same way. - **The two `property` enums are named `$defs`.** With them inline, cargo-typify 0.8.0 names both `StructuredDataSourceProperty`, keeps only the first, and Rust then refuses `{"kind":"site","property":"name"}`. The mechanical check below caught it. -- **Checked on this branch, then reverted:** with this diff, `pnpm gen` succeeded for TypeScript (`StructuredDataSource = FieldValueSource | EntryValueSource | SiteValueSource | TextValueSource`) and Rust (`#[serde(tag = "kind", deny_unknown_fields)] enum StructuredDataSource`). The core-contracts tests passed (492), and `cargo test -p cloudhal-schema` passed (19) with these fixtures: +- **Checked on this branch, then reverted:** with this diff, `pnpm gen` succeeded for TypeScript (`StructuredDataSource = FieldValueSource | EntryValueSource | SiteValueSource | ConstantValueSource`) and Rust (`#[serde(tag = "kind", deny_unknown_fields)] enum StructuredDataSource`). The core-contracts tests passed (492), and `cargo test -p cloudhal-schema` passed (19) with these fixtures: - `valid/site/collection-stage3.json` (an `events` collection with `listFields`, `expiry` and an `Event` preset using all four source kinds) and `valid/section/content-list-where.json` (`startsAt gte now`, `status in [scheduled]`, `orderBy startsAt asc`). - - `invalid/site/`: preset type `FAQPage`; path `Location`; source kind `html`; a control character in a text constant; a field source with a `value`; expiry action `delete`; a grace of 20,000 minutes; five list fields; a duplicate list field. + - `invalid/site/`: preset type `FAQPage`; path `Location`; source kind `html`; a control character in a constant; a field source with a `value`; expiry action `delete`; a grace of 20,000 minutes; five list fields; a duplicate list field. - `invalid/section/`: op `like`; twelve conditions; direction `up`. ### 2. JSON-LD presets diff --git a/packages/core-contracts/fixtures/invalid/site/expiry-action-delete.json b/packages/core-contracts/fixtures/invalid/site/expiry-action-delete.json index 2b8e45d6..a10c3b59 100644 --- a/packages/core-contracts/fixtures/invalid/site/expiry-action-delete.json +++ b/packages/core-contracts/fixtures/invalid/site/expiry-action-delete.json @@ -171,7 +171,7 @@ "field": "city" }, "location.address.addressCountry": { - "kind": "text", + "kind": "constant", "value": "TR" }, "eventStatus": { @@ -191,7 +191,7 @@ "field": "price" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "TRY" }, "offers.url": { diff --git a/packages/core-contracts/fixtures/invalid/site/expiry-extra-property.json b/packages/core-contracts/fixtures/invalid/site/expiry-extra-property.json index 4bd42b6a..dca614bf 100644 --- a/packages/core-contracts/fixtures/invalid/site/expiry-extra-property.json +++ b/packages/core-contracts/fixtures/invalid/site/expiry-extra-property.json @@ -172,7 +172,7 @@ "field": "city" }, "location.address.addressCountry": { - "kind": "text", + "kind": "constant", "value": "TR" }, "eventStatus": { @@ -192,7 +192,7 @@ "field": "price" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "TRY" }, "offers.url": { diff --git a/packages/core-contracts/fixtures/invalid/site/expiry-field-kebab.json b/packages/core-contracts/fixtures/invalid/site/expiry-field-kebab.json index 0e3711d8..8e5a5717 100644 --- a/packages/core-contracts/fixtures/invalid/site/expiry-field-kebab.json +++ b/packages/core-contracts/fixtures/invalid/site/expiry-field-kebab.json @@ -171,7 +171,7 @@ "field": "city" }, "location.address.addressCountry": { - "kind": "text", + "kind": "constant", "value": "TR" }, "eventStatus": { @@ -191,7 +191,7 @@ "field": "price" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "TRY" }, "offers.url": { diff --git a/packages/core-contracts/fixtures/invalid/site/expiry-grace-fraction.json b/packages/core-contracts/fixtures/invalid/site/expiry-grace-fraction.json index cad79a47..f39c53de 100644 --- a/packages/core-contracts/fixtures/invalid/site/expiry-grace-fraction.json +++ b/packages/core-contracts/fixtures/invalid/site/expiry-grace-fraction.json @@ -171,7 +171,7 @@ "field": "city" }, "location.address.addressCountry": { - "kind": "text", + "kind": "constant", "value": "TR" }, "eventStatus": { @@ -191,7 +191,7 @@ "field": "price" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "TRY" }, "offers.url": { diff --git a/packages/core-contracts/fixtures/invalid/site/expiry-grace-negative.json b/packages/core-contracts/fixtures/invalid/site/expiry-grace-negative.json index e810c670..4e0541ce 100644 --- a/packages/core-contracts/fixtures/invalid/site/expiry-grace-negative.json +++ b/packages/core-contracts/fixtures/invalid/site/expiry-grace-negative.json @@ -171,7 +171,7 @@ "field": "city" }, "location.address.addressCountry": { - "kind": "text", + "kind": "constant", "value": "TR" }, "eventStatus": { @@ -191,7 +191,7 @@ "field": "price" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "TRY" }, "offers.url": { diff --git a/packages/core-contracts/fixtures/invalid/site/expiry-grace-too-long.json b/packages/core-contracts/fixtures/invalid/site/expiry-grace-too-long.json index 5575a6ba..b9b932bd 100644 --- a/packages/core-contracts/fixtures/invalid/site/expiry-grace-too-long.json +++ b/packages/core-contracts/fixtures/invalid/site/expiry-grace-too-long.json @@ -171,7 +171,7 @@ "field": "city" }, "location.address.addressCountry": { - "kind": "text", + "kind": "constant", "value": "TR" }, "eventStatus": { @@ -191,7 +191,7 @@ "field": "price" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "TRY" }, "offers.url": { diff --git a/packages/core-contracts/fixtures/invalid/site/expiry-without-action.json b/packages/core-contracts/fixtures/invalid/site/expiry-without-action.json index 481a8d32..5bc9bb5f 100644 --- a/packages/core-contracts/fixtures/invalid/site/expiry-without-action.json +++ b/packages/core-contracts/fixtures/invalid/site/expiry-without-action.json @@ -170,7 +170,7 @@ "field": "city" }, "location.address.addressCountry": { - "kind": "text", + "kind": "constant", "value": "TR" }, "eventStatus": { @@ -190,7 +190,7 @@ "field": "price" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "TRY" }, "offers.url": { diff --git a/packages/core-contracts/fixtures/invalid/site/list-fields-bad-key.json b/packages/core-contracts/fixtures/invalid/site/list-fields-bad-key.json index 21c37f06..2f20b2f1 100644 --- a/packages/core-contracts/fixtures/invalid/site/list-fields-bad-key.json +++ b/packages/core-contracts/fixtures/invalid/site/list-fields-bad-key.json @@ -171,7 +171,7 @@ "field": "city" }, "location.address.addressCountry": { - "kind": "text", + "kind": "constant", "value": "TR" }, "eventStatus": { @@ -191,7 +191,7 @@ "field": "price" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "TRY" }, "offers.url": { diff --git a/packages/core-contracts/fixtures/invalid/site/list-fields-duplicate.json b/packages/core-contracts/fixtures/invalid/site/list-fields-duplicate.json index a4ba8ab5..3fbce4ba 100644 --- a/packages/core-contracts/fixtures/invalid/site/list-fields-duplicate.json +++ b/packages/core-contracts/fixtures/invalid/site/list-fields-duplicate.json @@ -171,7 +171,7 @@ "field": "city" }, "location.address.addressCountry": { - "kind": "text", + "kind": "constant", "value": "TR" }, "eventStatus": { @@ -191,7 +191,7 @@ "field": "price" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "TRY" }, "offers.url": { diff --git a/packages/core-contracts/fixtures/invalid/site/list-fields-empty.json b/packages/core-contracts/fixtures/invalid/site/list-fields-empty.json index b0716fcc..ffcfefa6 100644 --- a/packages/core-contracts/fixtures/invalid/site/list-fields-empty.json +++ b/packages/core-contracts/fixtures/invalid/site/list-fields-empty.json @@ -171,7 +171,7 @@ "field": "city" }, "location.address.addressCountry": { - "kind": "text", + "kind": "constant", "value": "TR" }, "eventStatus": { @@ -191,7 +191,7 @@ "field": "price" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "TRY" }, "offers.url": { diff --git a/packages/core-contracts/fixtures/invalid/site/list-fields-too-many.json b/packages/core-contracts/fixtures/invalid/site/list-fields-too-many.json index 8db4ced5..80f380a1 100644 --- a/packages/core-contracts/fixtures/invalid/site/list-fields-too-many.json +++ b/packages/core-contracts/fixtures/invalid/site/list-fields-too-many.json @@ -171,7 +171,7 @@ "field": "city" }, "location.address.addressCountry": { - "kind": "text", + "kind": "constant", "value": "TR" }, "eventStatus": { @@ -191,7 +191,7 @@ "field": "price" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "TRY" }, "offers.url": { diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-text-control-char.json b/packages/core-contracts/fixtures/invalid/site/structured-data-constant-control-char.json similarity index 98% rename from packages/core-contracts/fixtures/invalid/site/structured-data-text-control-char.json rename to packages/core-contracts/fixtures/invalid/site/structured-data-constant-control-char.json index b48426d4..6059567d 100644 --- a/packages/core-contracts/fixtures/invalid/site/structured-data-text-control-char.json +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-constant-control-char.json @@ -171,7 +171,7 @@ "field": "city" }, "location.address.addressCountry": { - "kind": "text", + "kind": "constant", "value": "TR" }, "eventStatus": { @@ -191,7 +191,7 @@ "field": "price" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "TRY" }, "offers.url": { @@ -203,7 +203,7 @@ "field": "poster" }, "description": { - "kind": "text", + "kind": "constant", "value": "line\nbreak" } } diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-text-empty.json b/packages/core-contracts/fixtures/invalid/site/structured-data-constant-empty.json similarity index 98% rename from packages/core-contracts/fixtures/invalid/site/structured-data-text-empty.json rename to packages/core-contracts/fixtures/invalid/site/structured-data-constant-empty.json index 1ac1d185..76d3488e 100644 --- a/packages/core-contracts/fixtures/invalid/site/structured-data-text-empty.json +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-constant-empty.json @@ -171,7 +171,7 @@ "field": "city" }, "location.address.addressCountry": { - "kind": "text", + "kind": "constant", "value": "TR" }, "eventStatus": { @@ -191,7 +191,7 @@ "field": "price" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "TRY" }, "offers.url": { @@ -203,7 +203,7 @@ "field": "poster" }, "description": { - "kind": "text", + "kind": "constant", "value": "" } } diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-text-too-long.json b/packages/core-contracts/fixtures/invalid/site/structured-data-constant-too-long.json similarity index 98% rename from packages/core-contracts/fixtures/invalid/site/structured-data-text-too-long.json rename to packages/core-contracts/fixtures/invalid/site/structured-data-constant-too-long.json index 2e606a8b..22f26893 100644 --- a/packages/core-contracts/fixtures/invalid/site/structured-data-text-too-long.json +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-constant-too-long.json @@ -171,7 +171,7 @@ "field": "city" }, "location.address.addressCountry": { - "kind": "text", + "kind": "constant", "value": "TR" }, "eventStatus": { @@ -191,7 +191,7 @@ "field": "price" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "TRY" }, "offers.url": { @@ -203,7 +203,7 @@ "field": "poster" }, "description": { - "kind": "text", + "kind": "constant", "value": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } } diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-entry-unknown-property.json b/packages/core-contracts/fixtures/invalid/site/structured-data-entry-unknown-property.json index 1c3a5951..421e54f5 100644 --- a/packages/core-contracts/fixtures/invalid/site/structured-data-entry-unknown-property.json +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-entry-unknown-property.json @@ -171,7 +171,7 @@ "field": "city" }, "location.address.addressCountry": { - "kind": "text", + "kind": "constant", "value": "TR" }, "eventStatus": { @@ -191,7 +191,7 @@ "field": "price" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "TRY" }, "offers.url": { diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-extra-property.json b/packages/core-contracts/fixtures/invalid/site/structured-data-extra-property.json index cf4fcd5c..50df5904 100644 --- a/packages/core-contracts/fixtures/invalid/site/structured-data-extra-property.json +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-extra-property.json @@ -171,7 +171,7 @@ "field": "city" }, "location.address.addressCountry": { - "kind": "text", + "kind": "constant", "value": "TR" }, "eventStatus": { @@ -191,7 +191,7 @@ "field": "price" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "TRY" }, "offers.url": { diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-faq-page.json b/packages/core-contracts/fixtures/invalid/site/structured-data-faq-page.json index ede104da..dcb512c0 100644 --- a/packages/core-contracts/fixtures/invalid/site/structured-data-faq-page.json +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-faq-page.json @@ -171,7 +171,7 @@ "field": "city" }, "location.address.addressCountry": { - "kind": "text", + "kind": "constant", "value": "TR" }, "eventStatus": { @@ -191,7 +191,7 @@ "field": "price" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "TRY" }, "offers.url": { diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-field-missing.json b/packages/core-contracts/fixtures/invalid/site/structured-data-field-missing.json index 5ef6d9ab..12105b84 100644 --- a/packages/core-contracts/fixtures/invalid/site/structured-data-field-missing.json +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-field-missing.json @@ -170,7 +170,7 @@ "field": "city" }, "location.address.addressCountry": { - "kind": "text", + "kind": "constant", "value": "TR" }, "eventStatus": { @@ -190,7 +190,7 @@ "field": "price" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "TRY" }, "offers.url": { diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-field-with-value.json b/packages/core-contracts/fixtures/invalid/site/structured-data-field-with-value.json index 3740f3e1..26225180 100644 --- a/packages/core-contracts/fixtures/invalid/site/structured-data-field-with-value.json +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-field-with-value.json @@ -172,7 +172,7 @@ "field": "city" }, "location.address.addressCountry": { - "kind": "text", + "kind": "constant", "value": "TR" }, "eventStatus": { @@ -192,7 +192,7 @@ "field": "price" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "TRY" }, "offers.url": { diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-kind-html.json b/packages/core-contracts/fixtures/invalid/site/structured-data-kind-html.json index 989c44f2..7244212a 100644 --- a/packages/core-contracts/fixtures/invalid/site/structured-data-kind-html.json +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-kind-html.json @@ -171,7 +171,7 @@ "field": "city" }, "location.address.addressCountry": { - "kind": "text", + "kind": "constant", "value": "TR" }, "eventStatus": { @@ -191,7 +191,7 @@ "field": "price" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "TRY" }, "offers.url": { diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-path-digit.json b/packages/core-contracts/fixtures/invalid/site/structured-data-path-digit.json index d615dcf3..8330e234 100644 --- a/packages/core-contracts/fixtures/invalid/site/structured-data-path-digit.json +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-path-digit.json @@ -171,7 +171,7 @@ "field": "city" }, "location.address.addressCountry": { - "kind": "text", + "kind": "constant", "value": "TR" }, "eventStatus": { @@ -191,7 +191,7 @@ "field": "price" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "TRY" }, "offers.url": { @@ -203,7 +203,7 @@ "field": "poster" }, "offers2.price": { - "kind": "text", + "kind": "constant", "value": "1" } } diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-path-too-deep.json b/packages/core-contracts/fixtures/invalid/site/structured-data-path-too-deep.json index 697b09a5..8a4c0910 100644 --- a/packages/core-contracts/fixtures/invalid/site/structured-data-path-too-deep.json +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-path-too-deep.json @@ -171,7 +171,7 @@ "field": "city" }, "location.address.addressCountry": { - "kind": "text", + "kind": "constant", "value": "TR" }, "eventStatus": { @@ -191,7 +191,7 @@ "field": "price" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "TRY" }, "offers.url": { @@ -203,7 +203,7 @@ "field": "poster" }, "location.address.geo.latitude": { - "kind": "text", + "kind": "constant", "value": "1" } } diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-path-upper.json b/packages/core-contracts/fixtures/invalid/site/structured-data-path-upper.json index 8c9eeec1..2d218bae 100644 --- a/packages/core-contracts/fixtures/invalid/site/structured-data-path-upper.json +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-path-upper.json @@ -171,7 +171,7 @@ "field": "city" }, "location.address.addressCountry": { - "kind": "text", + "kind": "constant", "value": "TR" }, "eventStatus": { @@ -191,7 +191,7 @@ "field": "price" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "TRY" }, "offers.url": { @@ -203,7 +203,7 @@ "field": "poster" }, "Location": { - "kind": "text", + "kind": "constant", "value": "x" } } diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-site-entry-property.json b/packages/core-contracts/fixtures/invalid/site/structured-data-site-entry-property.json index 5ee76a11..04a7e82b 100644 --- a/packages/core-contracts/fixtures/invalid/site/structured-data-site-entry-property.json +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-site-entry-property.json @@ -171,7 +171,7 @@ "field": "city" }, "location.address.addressCountry": { - "kind": "text", + "kind": "constant", "value": "TR" }, "eventStatus": { @@ -191,7 +191,7 @@ "field": "price" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "TRY" }, "offers.url": { diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-site-unknown-property.json b/packages/core-contracts/fixtures/invalid/site/structured-data-site-unknown-property.json index c058622d..5ef0a883 100644 --- a/packages/core-contracts/fixtures/invalid/site/structured-data-site-unknown-property.json +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-site-unknown-property.json @@ -171,7 +171,7 @@ "field": "city" }, "location.address.addressCountry": { - "kind": "text", + "kind": "constant", "value": "TR" }, "eventStatus": { @@ -191,7 +191,7 @@ "field": "price" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "TRY" }, "offers.url": { diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-too-many-properties.json b/packages/core-contracts/fixtures/invalid/site/structured-data-too-many-properties.json index c11383ab..62adbfe9 100644 --- a/packages/core-contracts/fixtures/invalid/site/structured-data-too-many-properties.json +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-too-many-properties.json @@ -151,167 +151,167 @@ "type": "Event", "properties": { "pa": { - "kind": "text", + "kind": "constant", "value": "x" }, "pb": { - "kind": "text", + "kind": "constant", "value": "x" }, "pc": { - "kind": "text", + "kind": "constant", "value": "x" }, "pd": { - "kind": "text", + "kind": "constant", "value": "x" }, "pe": { - "kind": "text", + "kind": "constant", "value": "x" }, "pf": { - "kind": "text", + "kind": "constant", "value": "x" }, "pg": { - "kind": "text", + "kind": "constant", "value": "x" }, "ph": { - "kind": "text", + "kind": "constant", "value": "x" }, "pi": { - "kind": "text", + "kind": "constant", "value": "x" }, "pj": { - "kind": "text", + "kind": "constant", "value": "x" }, "pk": { - "kind": "text", + "kind": "constant", "value": "x" }, "pl": { - "kind": "text", + "kind": "constant", "value": "x" }, "pm": { - "kind": "text", + "kind": "constant", "value": "x" }, "pn": { - "kind": "text", + "kind": "constant", "value": "x" }, "po": { - "kind": "text", + "kind": "constant", "value": "x" }, "pp": { - "kind": "text", + "kind": "constant", "value": "x" }, "pq": { - "kind": "text", + "kind": "constant", "value": "x" }, "pr": { - "kind": "text", + "kind": "constant", "value": "x" }, "ps": { - "kind": "text", + "kind": "constant", "value": "x" }, "pt": { - "kind": "text", + "kind": "constant", "value": "x" }, "pu": { - "kind": "text", + "kind": "constant", "value": "x" }, "pv": { - "kind": "text", + "kind": "constant", "value": "x" }, "pw": { - "kind": "text", + "kind": "constant", "value": "x" }, "px": { - "kind": "text", + "kind": "constant", "value": "x" }, "py": { - "kind": "text", + "kind": "constant", "value": "x" }, "pz": { - "kind": "text", + "kind": "constant", "value": "x" }, "pA": { - "kind": "text", + "kind": "constant", "value": "x" }, "pB": { - "kind": "text", + "kind": "constant", "value": "x" }, "pC": { - "kind": "text", + "kind": "constant", "value": "x" }, "pD": { - "kind": "text", + "kind": "constant", "value": "x" }, "pE": { - "kind": "text", + "kind": "constant", "value": "x" }, "pF": { - "kind": "text", + "kind": "constant", "value": "x" }, "pG": { - "kind": "text", + "kind": "constant", "value": "x" }, "pH": { - "kind": "text", + "kind": "constant", "value": "x" }, "pI": { - "kind": "text", + "kind": "constant", "value": "x" }, "pJ": { - "kind": "text", + "kind": "constant", "value": "x" }, "pK": { - "kind": "text", + "kind": "constant", "value": "x" }, "pL": { - "kind": "text", + "kind": "constant", "value": "x" }, "pM": { - "kind": "text", + "kind": "constant", "value": "x" }, "pN": { - "kind": "text", + "kind": "constant", "value": "x" }, "pO": { - "kind": "text", + "kind": "constant", "value": "x" } } diff --git a/packages/core-contracts/fixtures/invalid/site/structured-data-without-type.json b/packages/core-contracts/fixtures/invalid/site/structured-data-without-type.json index 6d70169a..a4068f1b 100644 --- a/packages/core-contracts/fixtures/invalid/site/structured-data-without-type.json +++ b/packages/core-contracts/fixtures/invalid/site/structured-data-without-type.json @@ -170,7 +170,7 @@ "field": "city" }, "location.address.addressCountry": { - "kind": "text", + "kind": "constant", "value": "TR" }, "eventStatus": { @@ -190,7 +190,7 @@ "field": "price" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "TRY" }, "offers.url": { diff --git a/packages/core-contracts/fixtures/valid/site/collection-stage3-presets.json b/packages/core-contracts/fixtures/valid/site/collection-stage3-presets.json index 1006f934..1dc25644 100644 --- a/packages/core-contracts/fixtures/valid/site/collection-stage3-presets.json +++ b/packages/core-contracts/fixtures/valid/site/collection-stage3-presets.json @@ -74,11 +74,11 @@ "field": "remote" }, "applicantLocationRequirements.name": { - "kind": "text", + "kind": "constant", "value": "Türkiye" }, "employmentType": { - "kind": "text", + "kind": "constant", "value": "FULL_TIME" } } @@ -105,7 +105,7 @@ "property": "updatedAt" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "EUR" } } diff --git a/packages/core-contracts/fixtures/valid/site/collection-stage3.json b/packages/core-contracts/fixtures/valid/site/collection-stage3.json index 22f96580..d563e44f 100644 --- a/packages/core-contracts/fixtures/valid/site/collection-stage3.json +++ b/packages/core-contracts/fixtures/valid/site/collection-stage3.json @@ -169,7 +169,7 @@ "field": "city" }, "location.address.addressCountry": { - "kind": "text", + "kind": "constant", "value": "TR" }, "eventStatus": { @@ -189,7 +189,7 @@ "field": "price" }, "offers.priceCurrency": { - "kind": "text", + "kind": "constant", "value": "TRY" }, "offers.url": { diff --git a/packages/core-contracts/schemas/site.schema.json b/packages/core-contracts/schemas/site.schema.json index 79256312..c58400bc 100644 --- a/packages/core-contracts/schemas/site.schema.json +++ b/packages/core-contracts/schemas/site.schema.json @@ -231,7 +231,7 @@ } }, { - "title": "TextValueSource", + "title": "ConstantValueSource", "type": "object", "description": "A plain-text constant: no control characters, no markup meaning; vocabulary-checked on enum paths (in code).", "additionalProperties": false, @@ -239,7 +239,7 @@ "properties": { "kind": { "type": "string", - "enum": ["text"] + "enum": ["constant"] }, "value": { "type": "string", diff --git a/packages/core-contracts/src/generated/index.ts b/packages/core-contracts/src/generated/index.ts index 5926f38d..df939360 100644 --- a/packages/core-contracts/src/generated/index.ts +++ b/packages/core-contracts/src/generated/index.ts @@ -6,7 +6,7 @@ */ export type { SchemaVersion, Id, Slug, Revision, Timestamp, Project, Metadata } from "./project.ts"; -export type { Locale, FieldDefinition, FieldKey, StructuredDataSource, EntryValueProperty, SiteValueProperty, Section, BodyProfile, ActorKind, BodyBlocks, SurfaceComponentId, Path, Section1, TimeZone, Site, Domain, Localization, CollectionSettings, CollectionLabel, TextField, FieldLabels, FieldLabel, FieldMigration, RichTextField, NumberField, BooleanField, DateField, DateTimeField, UrlField, ImageField, SelectField, FieldOption, CollectionExpiry, StructuredDataPreset, FieldValueSource, EntryValueSource, SiteValueSource, TextValueSource, ThemeRef, Design, Brand, ImageRef, SocialLink, Chrome, ChromeHeader, HeaderAction, ChromeFooter, FooterColumn, HeroSection, Link, RichTextSection, BodyBlock, Actor, ImageSection, ContentListSection, FieldCondition, FieldOrder, ComponentSection, SurfaceSection, SurfaceComponent, Announcement, Consent } from "./site.ts"; +export type { Locale, FieldDefinition, FieldKey, StructuredDataSource, EntryValueProperty, SiteValueProperty, Section, BodyProfile, ActorKind, BodyBlocks, SurfaceComponentId, Path, Section1, TimeZone, Site, Domain, Localization, CollectionSettings, CollectionLabel, TextField, FieldLabels, FieldLabel, FieldMigration, RichTextField, NumberField, BooleanField, DateField, DateTimeField, UrlField, ImageField, SelectField, FieldOption, CollectionExpiry, StructuredDataPreset, FieldValueSource, EntryValueSource, SiteValueSource, ConstantValueSource, ThemeRef, Design, Brand, ImageRef, SocialLink, Chrome, ChromeHeader, HeaderAction, ChromeFooter, FooterColumn, HeroSection, Link, RichTextSection, BodyBlock, Actor, ImageSection, ContentListSection, FieldCondition, FieldOrder, ComponentSection, SurfaceSection, SurfaceComponent, Announcement, Consent } from "./site.ts"; export type { 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 7f089a10..dad84ff9 100644 --- a/packages/core-contracts/src/generated/schemas.ts +++ b/packages/core-contracts/src/generated/schemas.ts @@ -1880,7 +1880,7 @@ export const SiteSchema = { } }, { - "title": "TextValueSource", + "title": "ConstantValueSource", "type": "object", "description": "A plain-text constant: no control characters, no markup meaning; vocabulary-checked on enum paths (in code).", "additionalProperties": false, @@ -1892,7 +1892,7 @@ export const SiteSchema = { "kind": { "type": "string", "enum": [ - "text" + "constant" ] }, "value": { diff --git a/packages/core-contracts/src/generated/site.ts b/packages/core-contracts/src/generated/site.ts index 4aacd250..a0df6956 100644 --- a/packages/core-contracts/src/generated/site.ts +++ b/packages/core-contracts/src/generated/site.ts @@ -63,7 +63,7 @@ export type FieldKey = string; * via the `definition` "StructuredDataSource". */ export type StructuredDataSource = - FieldValueSource | EntryValueSource | SiteValueSource | TextValueSource; + FieldValueSource | EntryValueSource | SiteValueSource | ConstantValueSource; /** * RFC-0042: entry properties a structured-data property may read. * @@ -590,8 +590,8 @@ export interface SiteValueSource { /** * A plain-text constant: no control characters, no markup meaning; vocabulary-checked on enum paths (in code). */ -export interface TextValueSource { - kind: "text"; +export interface ConstantValueSource { + kind: "constant"; value: string; } /** diff --git a/packages/core-contracts/src/generated/validators.js b/packages/core-contracts/src/generated/validators.js index fba42f20..0d017158 100644 --- a/packages/core-contracts/src/generated/validators.js +++ b/packages/core-contracts/src/generated/validators.js @@ -530,7 +530,7 @@ return errors === 0; validate20.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; export const validateSite = validate21; -const schema41 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://cloudhal.dev/schemas/v1/site.schema.json","title":"Site","description":"Root node of a Site Graph: one deployable website with its locales, domains, theme and page index. Pages and content entries are separate nodes referenced by id so they can be versioned and locked independently.","type":"object","additionalProperties":false,"required":["schemaVersion","id","projectId","name","slug","defaultLocale","locales","domains","pageIds","theme","rev","createdAt","updatedAt"],"properties":{"schemaVersion":{"$ref":"#/$defs/SchemaVersion"},"id":{"$ref":"#/$defs/Id"},"projectId":{"$ref":"#/$defs/Id"},"name":{"type":"string","minLength":1,"maxLength":200},"slug":{"$ref":"#/$defs/Slug"},"defaultLocale":{"$ref":"#/$defs/Locale"},"locales":{"type":"array","items":{"$ref":"#/$defs/Locale"},"minItems":1,"uniqueItems":true},"domains":{"type":"array","description":"Hostnames serving this site. Exactly one should be primary: the address canonical links, feeds and previews use. Sites run on their own domains or their Cloudflare account's workers.dev address, never on a Cloudhal domain (ADR-0037).","items":{"$ref":"#/$defs/Domain"}},"pageIds":{"type":"array","items":{"$ref":"#/$defs/Id"},"uniqueItems":true},"homePageId":{"$ref":"#/$defs/Id"},"localization":{"$ref":"#/$defs/Localization"},"collections":{"type":"object","description":"RFC-0038, RFC-0040: the site's declared collections and their settings, keyed by collection (ContentEntry.collection). A key here declares the collection, even with no settings ({}). A collection with entries but no key is inferred and uses the defaults.","propertyNames":{"$ref":"#/$defs/Slug"},"maxProperties":50,"additionalProperties":{"$ref":"#/$defs/CollectionSettings"}},"theme":{"$ref":"#/$defs/ThemeRef"},"design":{"$ref":"#/$defs/Design"},"brand":{"$ref":"#/$defs/Brand"},"chrome":{"$ref":"#/$defs/Chrome"},"timeZone":{"$ref":"#/$defs/TimeZone","description":"The site's time zone (RFC-0008): Studio's calendar, schedules entered as wall-clock times and recurring automations use it. Absent means UTC."},"publishedSnapshotId":{"$ref":"#/$defs/Id","description":"Snapshot currently serving production. Rollback = pointing this at an older snapshot."},"rev":{"$ref":"#/$defs/Revision"},"createdAt":{"$ref":"#/$defs/Timestamp"},"updatedAt":{"$ref":"#/$defs/Timestamp"},"metadata":{"$ref":"#/$defs/Metadata"}},"$defs":{"Actor":{"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}},"ActorKind":{"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]},"Announcement":{"type":"object","additionalProperties":false,"required":["id","text"],"properties":{"id":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,40}$","description":"Change it when the text changes, so readers who dismissed the old one see the new one."},"text":{"type":"string","minLength":1,"maxLength":200},"label":{"type":"string","minLength":1,"maxLength":40},"href":{"type":"string","minLength":1,"maxLength":2048},"tone":{"type":"string","enum":["info","brand","warning"]},"dismissible":{"type":"boolean"}}},"BodyBlock":{"type":"object","description":"One top-level block of a markdown body in the store's index (RFC-0014 §3). Written by the store only: a client's values are replaced on every write. `by`, `via` and `at` say who last changed the block's content, through which channel and when; they are absent for content written before the index existed.","additionalProperties":false,"required":["id","hash"],"properties":{"id":{"type":"string","pattern":"^blk_[0-9A-Za-z]{8,24}$","description":"Stable across writes that keep the block's content or its place among unchanged blocks."},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"SHA-256 (hex) of the block's markdown without trailing whitespace."},"by":{"$ref":"#/$defs/Actor"},"via":{"type":"string","enum":["studio","mcp","api","import","ai-command","proposal"],"description":"The channel of that change."},"at":{"$ref":"#/$defs/Timestamp"}}},"BodyBlocks":{"type":"array","description":"A markdown body's block index, in body order (RFC-0014 §3). Absent means not indexed (yet, or an HTML body), never empty.","maxItems":5000,"items":{"$ref":"#/$defs/BodyBlock"}},"BodyProfile":{"type":"integer","enum":[1],"description":"The markdown dialect a body follows (RFC-0014 §1): 1 = Cloudhal Markdown v1 (CommonMark + GFM + `asset:` + directives; raw HTML escaped). Set by the store on markdown bodies."},"Brand":{"type":"object","description":"The site's logo and social links (RFC-0025 §1), shared by the header, the footer and SEO.","additionalProperties":false,"properties":{"logo":{"$ref":"#/$defs/ImageRef"},"logoDark":{"$ref":"#/$defs/ImageRef"},"logoHeight":{"type":"string","enum":["s","m","l"]},"social":{"type":"array","maxItems":8,"items":{"$ref":"#/$defs/SocialLink"}}}},"Chrome":{"type":"object","description":"The site's header, footer and bars as data (RFC-0025): enums, menu keys and short texts; the theme renders them with landmarks, focus and contrast guaranteed.","additionalProperties":false,"properties":{"header":{"$ref":"#/$defs/ChromeHeader"},"footer":{"$ref":"#/$defs/ChromeFooter"},"announcement":{"$ref":"#/$defs/Announcement"},"consent":{"$ref":"#/$defs/Consent"}}},"ChromeFooter":{"type":"object","additionalProperties":false,"properties":{"layout":{"type":"string","enum":["simple","columns","cta-band","minimal"]},"surface":{"type":"string","enum":["plain","muted","inverse"]},"tagline":{"type":"string","minLength":1,"maxLength":200},"columns":{"type":"array","maxItems":5,"items":{"$ref":"#/$defs/FooterColumn"}},"ctaHeading":{"type":"string","minLength":1,"maxLength":120},"ctaLabel":{"type":"string","minLength":1,"maxLength":40},"ctaHref":{"type":"string","minLength":1,"maxLength":2048},"social":{"type":"boolean"},"copyright":{"type":"string","minLength":1,"maxLength":200,"description":"The legal line; {year} and {site} are the only placeholders."},"legal":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."},"extras":{"type":"array","maxItems":3,"uniqueItems":true,"items":{"type":"string","enum":["locale","theme-toggle","motion-toggle"]}},"credit":{"type":"boolean"},"region":{"$ref":"#/$defs/Section","description":"RFC-0025 §2: one optional surface (kind `surface`) above the footer's columns, built and checked like any surface; the store refuses other section kinds."}}},"ChromeHeader":{"type":"object","additionalProperties":false,"properties":{"layout":{"type":"string","enum":["logo-left","logo-left-nav-center","centered","split","minimal"]},"behavior":{"type":"string","enum":["static","sticky","hide-on-scroll","fixed"]},"shrink":{"type":"boolean"},"overHero":{"type":"string","enum":["solid","transparent"]},"width":{"type":"string","enum":["content","wide","full"]},"surface":{"type":"string","enum":["plain","muted","raised","inverse","brand"]},"mobileStyle":{"type":"string","enum":["dropdown","drawer-start","drawer-end","fullscreen"]},"collapseAt":{"type":"string","enum":["sm","md","lg","always"]},"nav":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."},"utility":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."},"actions":{"type":"array","maxItems":3,"items":{"$ref":"#/$defs/HeaderAction"}}}},"CollectionExpiry":{"type":"object","description":"RFC-0042: a time gate every entry derives from one of its date or datetime fields, checked at render time like Availability.until. A date ends at the start of the next day in the site's time zone.","additionalProperties":false,"required":["field","action"],"properties":{"field":{"$ref":"#/$defs/FieldKey"},"action":{"type":"string","description":"unlist: the page stays, the entry leaves listings, feeds, the sitemap and search. hide: every public route answers 404.","enum":["unlist","hide"]},"afterMinutes":{"type":"integer","description":"Grace after the field's instant. Absent: 0.","minimum":0,"maximum":10080}}},"CollectionLabel":{"type":"object","additionalProperties":false,"required":["singular","plural"],"properties":{"singular":{"type":"string","minLength":1,"maxLength":100},"plural":{"type":"string","minLength":1,"maxLength":100},"description":{"type":"string","maxLength":1000}}},"CollectionSettings":{"type":"object","additionalProperties":false,"properties":{"permalink":{"type":"string","description":"RFC-0038: where the collection's entries live, as a template with a closed token set. Version 1: one or more fixed lower-case path segments followed by /{slug}, e.g. /blog/{slug}. Absent means //{slug}.","pattern":"^(/[a-z0-9]+(-[a-z0-9]+)*)+/\\{slug\\}$"},"labels":{"type":"object","description":"RFC-0040: display names per locale (Studio, agents, themes). Absent or missing a locale: the default locale's, then the key.","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"$ref":"#/$defs/CollectionLabel"}},"icon":{"type":"string","description":"RFC-0040: a name from Studio's closed icon set (e.g. calendar, briefcase, users). Unknown names show the default icon. Never a URL or markup.","pattern":"^[a-z0-9]+(-[a-z0-9]+)*$","maxLength":40},"feed":{"type":"boolean","description":"RFC-0040: entries appear in the site's RSS feeds. Absent: true."},"sitemap":{"type":"boolean","description":"RFC-0040: published entries appear in sitemap.xml. Absent: true."},"sort":{"type":"string","description":"RFC-0040: default order where none is given (Studio's list, entry queries without sort, MCP list_entries). Absent: newest.","enum":["newest","oldest","title"]},"fields":{"type":"array","description":"RFC-0041: the collection's typed custom fields, in form order. Values live in ContentEntry.fields under each key. At most 200 per site and unique keys (checked in code).","minItems":1,"maxItems":30,"items":{"$ref":"#/$defs/FieldDefinition"}},"listFields":{"type":"array","description":"RFC-0042: fields that listings, queries, Studio columns and MCP may filter and sort by, in column order. Keys of this collection's fields (checked in code).","minItems":1,"maxItems":4,"uniqueItems":true,"items":{"$ref":"#/$defs/FieldKey"}},"expiry":{"$ref":"#/$defs/CollectionExpiry"},"structuredData":{"$ref":"#/$defs/StructuredDataPreset"}}},"Consent":{"type":"object","additionalProperties":false,"required":["mode"],"properties":{"mode":{"type":"string","enum":["none","notice","choices"]},"policy":{"$ref":"#/$defs/Path"}}},"Design":{"type":"object","description":"The site's design rules for people and agents (RFC-0021): Markdown that agents read with the theme's DESIGN.md before they build. Guidance only: it changes neither rendering nor permissions.","additionalProperties":false,"required":["markdown"],"properties":{"markdown":{"type":"string","minLength":1,"maxLength":24000}}},"Domain":{"type":"object","additionalProperties":false,"required":["hostname","primary"],"properties":{"hostname":{"type":"string","pattern":"^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,63}$","maxLength":253},"primary":{"type":"boolean"}}},"EntryValueProperty":{"type":"string","description":"RFC-0042: entry properties a structured-data property may read.","enum":["title","summary","body","coverImage","url","publishedAt","updatedAt"]},"FieldCondition":{"type":"object","description":"RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`.","additionalProperties":false,"required":["field","op"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","in","set","unset"]},"value":{"type":"string","minLength":1,"maxLength":100},"values":{"type":"array","minItems":1,"maxItems":20,"uniqueItems":true,"items":{"type":"string","minLength":1,"maxLength":100}}}},"FieldDefinition":{"description":"RFC-0041: one typed custom field of a collection, discriminated by `type`.","oneOf":[{"title":"TextField","type":"object","description":"Plain text: a string of at most maxLength characters.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["text"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"maxLength":{"type":"integer","description":"Absent: 5000.","minimum":1,"maximum":5000},"multiline":{"type":"boolean","description":"Line breaks allowed. Absent: false."},"searchable":{"type":"boolean","description":"Its words go into site search. Absent: false."}}},{"title":"RichTextField","type":"object","description":"Cloudhal Markdown v1 (ADR-0028), at most 50 000 characters.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["richText"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"searchable":{"type":"boolean","description":"Its words go into site search. Absent: false."}}},{"title":"NumberField","type":"object","description":"A finite number.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["number"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"integer":{"type":"boolean","description":"Whole numbers only. Absent: false."},"minimum":{"type":"number"},"maximum":{"type":"number"}}},{"title":"BooleanField","type":"object","description":"true or false; absent reads as false, so it cannot be required to publish.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["boolean"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"DateField","type":"object","description":"A calendar day without a zone (YYYY-MM-DD).","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["date"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"DateTimeField","type":"object","description":"An instant with the zone it was entered in: {at, timeZone} (RFC-0008 §9.1).","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["datetime"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"UrlField","type":"object","description":"A link: https, http, mailto, tel, a site-relative path or a #fragment; at most 2048 characters.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["url"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"ImageField","type":"object","description":"An image Asset of the same site, by id (asset_…).","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["image"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"SelectField","type":"object","description":"One of a closed list of options, or several when multiple.","additionalProperties":false,"required":["key","type","labels","localized","options"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["select"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"options":{"type":"array","minItems":1,"maxItems":50,"items":{"$ref":"#/$defs/FieldOption"}},"multiple":{"type":"boolean","description":"The value is a list of option values. Absent: false."}}}]},"FieldKey":{"type":"string","description":"The field's key in ContentEntry.fields: lower camelCase, a JavaScript identifier. Unique in its collection; built-in entry property names are reserved (checked in code).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"FieldLabel":{"type":"object","additionalProperties":false,"required":["label"],"properties":{"label":{"type":"string","minLength":1,"maxLength":100},"help":{"type":"string","maxLength":300}}},"FieldLabels":{"type":"object","description":"The field's label and help per locale. Missing a locale: the default locale's, then the key.","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"$ref":"#/$defs/FieldLabel"}},"FieldMigration":{"type":"object","description":"RFC-0041 §6: present only while a field operation rewrites entries.","additionalProperties":false,"required":["id"],"properties":{"id":{"$ref":"#/$defs/Id","description":"The operation's \"before\" snapshot."},"fromKey":{"$ref":"#/$defs/FieldKey"},"fromType":{"type":"string","enum":["text","richText","number","boolean","date","datetime","url","image","select"]},"fromLocalized":{"type":"boolean"}}},"FieldOption":{"type":"object","description":"One option of a select field. Values are unique in the field (checked in code).","additionalProperties":false,"required":["value","labels"],"properties":{"value":{"$ref":"#/$defs/Slug","description":"Stored in entries, never shown."},"labels":{"type":"object","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"type":"string","minLength":1,"maxLength":100}}}},"FieldOrder":{"type":"object","description":"RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last.","additionalProperties":false,"required":["field","direction"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"direction":{"type":"string","enum":["asc","desc"]}}},"FooterColumn":{"type":"object","additionalProperties":false,"required":["heading","menu"],"properties":{"heading":{"type":"string","minLength":1,"maxLength":60},"menu":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."}}},"HeaderAction":{"type":"object","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["button","search","locale","theme-toggle","motion-toggle"]},"label":{"type":"string","minLength":1,"maxLength":40},"href":{"type":"string","minLength":1,"maxLength":2048},"variant":{"type":"string","enum":["primary","secondary"]}}},"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"ImageRef":{"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}},"Link":{"type":"object","additionalProperties":false,"required":["label","href"],"properties":{"label":{"type":"string","minLength":1,"maxLength":200},"href":{"type":"string","minLength":1,"maxLength":2048},"external":{"type":"boolean"}}},"Locale":{"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"},"Localization":{"type":"object","description":"How the site serves its locales (RFC-0033 §2). Absent fields mean: prefix-except-default routing, no fallback, no required review, the machine-translation notice until reviewed.","additionalProperties":false,"properties":{"routing":{"type":"string","description":"prefix-except-default: the default locale at the root, the others under //. prefix-all: every locale prefixed, / is a language chooser (x-default).","enum":["prefix-except-default","prefix-all"]},"domains":{"type":"object","description":"Optional hostname per locale; that locale is then served at the host's root.","propertyNames":{"$ref":"#/$defs/Locale"},"additionalProperties":{"type":"string","pattern":"^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,63}$","maxLength":253}},"fallback":{"type":"string","description":"none: a missing translation is not found. default-with-notice: the default locale's page is served with a notice and noindex.","enum":["none","default-with-notice"]},"review":{"type":"object","description":"Collections whose machine translations need a person's review before they publish ('pages' stands for pages).","additionalProperties":false,"properties":{"required":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"uniqueItems":true}}},"label":{"type":"string","description":"When readers see the machine-translation notice: unreviewed (until a person reviews) or always.","enum":["unreviewed","always"]}}},"Metadata":{"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}},"Path":{"type":"string","description":"Absolute URL path of a route the site owns: starts with '/', lower-case slug segments, no trailing slash except root.","pattern":"^/(?:[a-z0-9]+(?:-[a-z0-9]+)*(?:/[a-z0-9]+(?:-[a-z0-9]+)*)*)?$","maxLength":1024},"Revision":{"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0},"SchemaVersion":{"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]},"Section":{"title":"Section","description":"A block inside a page, discriminated by `kind`. Variants are written inline (not via $ref) and `kind` is a single-value string enum so that TypeScript gets a literal union and Rust gets a serde internally-tagged enum.","oneOf":[{"type":"object","title":"HeroSection","additionalProperties":false,"required":["id","kind","heading"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["hero"]},"heading":{"type":"string","minLength":1,"maxLength":300},"subheading":{"type":"string","maxLength":1000},"image":{"$ref":"#/$defs/ImageRef"},"cta":{"$ref":"#/$defs/Link"}}},{"type":"object","title":"RichTextSection","additionalProperties":false,"required":["id","kind","format","body"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["richText"]},"format":{"type":"string","enum":["markdown","html"]},"body":{"type":"string","maxLength":200000},"profile":{"$ref":"#/$defs/BodyProfile"},"blocks":{"$ref":"#/$defs/BodyBlocks"}}},{"type":"object","title":"ImageSection","additionalProperties":false,"required":["id","kind","image"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["image"]},"image":{"$ref":"#/$defs/ImageRef"},"caption":{"type":"string","maxLength":1000}}},{"type":"object","title":"ContentListSection","description":"Renders the newest entries of a content collection (e.g. blog posts).","additionalProperties":false,"required":["id","kind","collection","limit","sort"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["contentList"]},"collection":{"$ref":"#/$defs/Slug"},"limit":{"type":"integer","minimum":1,"maximum":100},"sort":{"type":"string","enum":["newest","oldest","title"]},"filter":{"type":"string","description":"Filter expression over term assignments (RFC-0002), e.g. 'category:news+tag:ai' or 'category:news>*'.","minLength":1,"maxLength":512},"where":{"type":"array","description":"RFC-0042: conditions on the collection's list fields, ANDed.","minItems":1,"maxItems":4,"items":{"$ref":"#/$defs/FieldCondition"}},"orderBy":{"$ref":"#/$defs/FieldOrder"}}},{"type":"object","title":"ComponentSection","description":"A typed block from the block registry (RFC-0015): `type` names a registered block type (one global namespace of English names), `props` follow that type's props schema, which the store validates.","additionalProperties":false,"required":["id","kind","type","props"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["component"]},"type":{"type":"string","pattern":"^[a-z][a-z0-9-]{1,40}$"},"props":{"type":"object","description":"Validated against the type's props schema by the store, not by this contract.","additionalProperties":true}}},{"type":"object","title":"SurfaceSection","description":"A generative UI surface (RFC-0020): a flat list of catalog components with a `root`, plus a data model their props may bind to. Written and streamed by agents as data, never code; the store checks the tree, the catalog and the limits.","additionalProperties":false,"required":["id","kind","catalog","components"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["surface"]},"catalog":{"type":"string","description":"The catalog the surface was written for: `@` (RFC-0018 manifest).","pattern":"^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"},"components":{"type":"array","minItems":1,"maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponent"}},"data":{"type":"object","description":"The surface's data model: JSON its components bind to with `{path}` (at most 64 KB serialized; the store enforces the size).","additionalProperties":true}}}]},"SiteValueProperty":{"type":"string","description":"RFC-0042: site properties a structured-data property may read.","enum":["name","url","logo"]},"Slug":{"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128},"SocialLink":{"type":"object","additionalProperties":false,"required":["network","href"],"properties":{"network":{"type":"string","enum":["x","bluesky","mastodon","github","linkedin","instagram","facebook","youtube","tiktok","threads","rss","email"]},"href":{"type":"string","minLength":1,"maxLength":2048}}},"StructuredDataPreset":{"type":"object","description":"RFC-0042: the JSON-LD type an entry page emits in place of BlogPosting, with its properties mapped to value sources. Paths, fitting field types and required properties are a closed table in code (@cloudhal/core-ts/structured-data).","additionalProperties":false,"required":["type","properties"],"properties":{"type":{"type":"string","enum":["Event","JobPosting","Product","Recipe"]},"properties":{"type":"object","description":"Keyed by property path, e.g. startDate or location.address.addressLocality.","propertyNames":{"type":"string","pattern":"^[a-z][A-Za-z]{0,39}(\\.[a-z][A-Za-z]{0,39}){0,2}$"},"minProperties":1,"maxProperties":40,"additionalProperties":{"$ref":"#/$defs/StructuredDataSource"}}}},"StructuredDataSource":{"description":"RFC-0042: where one JSON-LD property's value comes from, discriminated by `kind`.","oneOf":[{"title":"FieldValueSource","type":"object","description":"A typed field of this collection, converted by its type.","additionalProperties":false,"required":["kind","field"],"properties":{"kind":{"type":"string","enum":["field"]},"field":{"$ref":"#/$defs/FieldKey"}}},{"title":"EntryValueSource","type":"object","description":"A built-in property of the entry.","additionalProperties":false,"required":["kind","property"],"properties":{"kind":{"type":"string","enum":["entry"]},"property":{"$ref":"#/$defs/EntryValueProperty"}}},{"title":"SiteValueSource","type":"object","description":"A property of the site.","additionalProperties":false,"required":["kind","property"],"properties":{"kind":{"type":"string","enum":["site"]},"property":{"$ref":"#/$defs/SiteValueProperty"}}},{"title":"TextValueSource","type":"object","description":"A plain-text constant: no control characters, no markup meaning; vocabulary-checked on enum paths (in code).","additionalProperties":false,"required":["kind","value"],"properties":{"kind":{"type":"string","enum":["text"]},"value":{"type":"string","minLength":1,"maxLength":200,"pattern":"^[^\\x00-\\x1F\\x7F]+$"}}}]},"SurfaceComponent":{"type":"object","description":"One element of a generative UI surface (RFC-0020 §1), in a flat list: `component` names a catalog entry (a PascalCase built-in primitive such as `Stack`, or a registry block type such as `callout`), `children` refer to other ids of the same surface, and `props` follow the entry's schema, which the store checks. Field names follow A2UI v0.9.","additionalProperties":false,"required":["id","component"],"properties":{"id":{"$ref":"#/$defs/SurfaceComponentId"},"component":{"type":"string","pattern":"^(?:[A-Z][A-Za-z0-9]{1,40}|[a-z][a-z0-9-]{1,40})$"},"children":{"type":"array","maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponentId"}},"props":{"type":"object","description":"Literal values, `{path}` bindings into the surface's data, or `{call}` named functions; validated by the store against the catalog, not by this contract.","additionalProperties":true},"visible":{"type":"object","description":"A visibility condition over the surface's data (`eq`, `ne`, `gt`, `lt`, `and`, `or`, `not`); validated by the store.","additionalProperties":true},"motion":{"type":"object","description":"Motion presets for this component (RFC-0024 §2): `reveal`, `hover`, `speed`, `delay`, `stagger`, `replay` and later `scroll`, `loop`; enums and clamped numbers only, validated by the store against the catalog. The renderer turns them into CSS; agents never write CSS.","additionalProperties":true}}},"SurfaceComponentId":{"type":"string","description":"A component's id, unique within its surface; `root` is the tree's root (RFC-0020 §1).","pattern":"^[a-z][a-z0-9-]{0,40}$"},"ThemeRef":{"type":"object","description":"Theme (a.k.a. Experience) applied by the renderer. Config is theme-defined and validated by the theme's own schema, not here.","additionalProperties":false,"required":["name"],"properties":{"name":{"$ref":"#/$defs/Slug"},"version":{"type":"string","maxLength":64},"config":{"type":"object","additionalProperties":true}}},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"},"TimeZone":{"type":"string","description":"IANA time zone name from the tz database (RFC-0008), e.g. Europe/Istanbul, America/Argentina/Buenos_Aires, Etc/GMT+3 or UTC. Wall-clock times a person enters are kept with their zone, so the instant is recomputed when governments change the rules. The pattern bounds the shape only; writers also check the name against the runtime's Intl zone list.","pattern":"^(?:UTC|[A-Z][A-Za-z_-]+(?:/[A-Za-z0-9_+-]+){1,2})$","maxLength":64}}}; +const schema41 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://cloudhal.dev/schemas/v1/site.schema.json","title":"Site","description":"Root node of a Site Graph: one deployable website with its locales, domains, theme and page index. Pages and content entries are separate nodes referenced by id so they can be versioned and locked independently.","type":"object","additionalProperties":false,"required":["schemaVersion","id","projectId","name","slug","defaultLocale","locales","domains","pageIds","theme","rev","createdAt","updatedAt"],"properties":{"schemaVersion":{"$ref":"#/$defs/SchemaVersion"},"id":{"$ref":"#/$defs/Id"},"projectId":{"$ref":"#/$defs/Id"},"name":{"type":"string","minLength":1,"maxLength":200},"slug":{"$ref":"#/$defs/Slug"},"defaultLocale":{"$ref":"#/$defs/Locale"},"locales":{"type":"array","items":{"$ref":"#/$defs/Locale"},"minItems":1,"uniqueItems":true},"domains":{"type":"array","description":"Hostnames serving this site. Exactly one should be primary: the address canonical links, feeds and previews use. Sites run on their own domains or their Cloudflare account's workers.dev address, never on a Cloudhal domain (ADR-0037).","items":{"$ref":"#/$defs/Domain"}},"pageIds":{"type":"array","items":{"$ref":"#/$defs/Id"},"uniqueItems":true},"homePageId":{"$ref":"#/$defs/Id"},"localization":{"$ref":"#/$defs/Localization"},"collections":{"type":"object","description":"RFC-0038, RFC-0040: the site's declared collections and their settings, keyed by collection (ContentEntry.collection). A key here declares the collection, even with no settings ({}). A collection with entries but no key is inferred and uses the defaults.","propertyNames":{"$ref":"#/$defs/Slug"},"maxProperties":50,"additionalProperties":{"$ref":"#/$defs/CollectionSettings"}},"theme":{"$ref":"#/$defs/ThemeRef"},"design":{"$ref":"#/$defs/Design"},"brand":{"$ref":"#/$defs/Brand"},"chrome":{"$ref":"#/$defs/Chrome"},"timeZone":{"$ref":"#/$defs/TimeZone","description":"The site's time zone (RFC-0008): Studio's calendar, schedules entered as wall-clock times and recurring automations use it. Absent means UTC."},"publishedSnapshotId":{"$ref":"#/$defs/Id","description":"Snapshot currently serving production. Rollback = pointing this at an older snapshot."},"rev":{"$ref":"#/$defs/Revision"},"createdAt":{"$ref":"#/$defs/Timestamp"},"updatedAt":{"$ref":"#/$defs/Timestamp"},"metadata":{"$ref":"#/$defs/Metadata"}},"$defs":{"Actor":{"type":"object","additionalProperties":false,"required":["kind","id"],"properties":{"kind":{"$ref":"#/$defs/ActorKind"},"id":{"$ref":"#/$defs/Id"},"name":{"type":"string","maxLength":200}}},"ActorKind":{"type":"string","description":"Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.","enum":["human","agent","system"]},"Announcement":{"type":"object","additionalProperties":false,"required":["id","text"],"properties":{"id":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,40}$","description":"Change it when the text changes, so readers who dismissed the old one see the new one."},"text":{"type":"string","minLength":1,"maxLength":200},"label":{"type":"string","minLength":1,"maxLength":40},"href":{"type":"string","minLength":1,"maxLength":2048},"tone":{"type":"string","enum":["info","brand","warning"]},"dismissible":{"type":"boolean"}}},"BodyBlock":{"type":"object","description":"One top-level block of a markdown body in the store's index (RFC-0014 §3). Written by the store only: a client's values are replaced on every write. `by`, `via` and `at` say who last changed the block's content, through which channel and when; they are absent for content written before the index existed.","additionalProperties":false,"required":["id","hash"],"properties":{"id":{"type":"string","pattern":"^blk_[0-9A-Za-z]{8,24}$","description":"Stable across writes that keep the block's content or its place among unchanged blocks."},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"SHA-256 (hex) of the block's markdown without trailing whitespace."},"by":{"$ref":"#/$defs/Actor"},"via":{"type":"string","enum":["studio","mcp","api","import","ai-command","proposal"],"description":"The channel of that change."},"at":{"$ref":"#/$defs/Timestamp"}}},"BodyBlocks":{"type":"array","description":"A markdown body's block index, in body order (RFC-0014 §3). Absent means not indexed (yet, or an HTML body), never empty.","maxItems":5000,"items":{"$ref":"#/$defs/BodyBlock"}},"BodyProfile":{"type":"integer","enum":[1],"description":"The markdown dialect a body follows (RFC-0014 §1): 1 = Cloudhal Markdown v1 (CommonMark + GFM + `asset:` + directives; raw HTML escaped). Set by the store on markdown bodies."},"Brand":{"type":"object","description":"The site's logo and social links (RFC-0025 §1), shared by the header, the footer and SEO.","additionalProperties":false,"properties":{"logo":{"$ref":"#/$defs/ImageRef"},"logoDark":{"$ref":"#/$defs/ImageRef"},"logoHeight":{"type":"string","enum":["s","m","l"]},"social":{"type":"array","maxItems":8,"items":{"$ref":"#/$defs/SocialLink"}}}},"Chrome":{"type":"object","description":"The site's header, footer and bars as data (RFC-0025): enums, menu keys and short texts; the theme renders them with landmarks, focus and contrast guaranteed.","additionalProperties":false,"properties":{"header":{"$ref":"#/$defs/ChromeHeader"},"footer":{"$ref":"#/$defs/ChromeFooter"},"announcement":{"$ref":"#/$defs/Announcement"},"consent":{"$ref":"#/$defs/Consent"}}},"ChromeFooter":{"type":"object","additionalProperties":false,"properties":{"layout":{"type":"string","enum":["simple","columns","cta-band","minimal"]},"surface":{"type":"string","enum":["plain","muted","inverse"]},"tagline":{"type":"string","minLength":1,"maxLength":200},"columns":{"type":"array","maxItems":5,"items":{"$ref":"#/$defs/FooterColumn"}},"ctaHeading":{"type":"string","minLength":1,"maxLength":120},"ctaLabel":{"type":"string","minLength":1,"maxLength":40},"ctaHref":{"type":"string","minLength":1,"maxLength":2048},"social":{"type":"boolean"},"copyright":{"type":"string","minLength":1,"maxLength":200,"description":"The legal line; {year} and {site} are the only placeholders."},"legal":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."},"extras":{"type":"array","maxItems":3,"uniqueItems":true,"items":{"type":"string","enum":["locale","theme-toggle","motion-toggle"]}},"credit":{"type":"boolean"},"region":{"$ref":"#/$defs/Section","description":"RFC-0025 §2: one optional surface (kind `surface`) above the footer's columns, built and checked like any surface; the store refuses other section kinds."}}},"ChromeHeader":{"type":"object","additionalProperties":false,"properties":{"layout":{"type":"string","enum":["logo-left","logo-left-nav-center","centered","split","minimal"]},"behavior":{"type":"string","enum":["static","sticky","hide-on-scroll","fixed"]},"shrink":{"type":"boolean"},"overHero":{"type":"string","enum":["solid","transparent"]},"width":{"type":"string","enum":["content","wide","full"]},"surface":{"type":"string","enum":["plain","muted","raised","inverse","brand"]},"mobileStyle":{"type":"string","enum":["dropdown","drawer-start","drawer-end","fullscreen"]},"collapseAt":{"type":"string","enum":["sm","md","lg","always"]},"nav":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."},"utility":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."},"actions":{"type":"array","maxItems":3,"items":{"$ref":"#/$defs/HeaderAction"}}}},"CollectionExpiry":{"type":"object","description":"RFC-0042: a time gate every entry derives from one of its date or datetime fields, checked at render time like Availability.until. A date ends at the start of the next day in the site's time zone.","additionalProperties":false,"required":["field","action"],"properties":{"field":{"$ref":"#/$defs/FieldKey"},"action":{"type":"string","description":"unlist: the page stays, the entry leaves listings, feeds, the sitemap and search. hide: every public route answers 404.","enum":["unlist","hide"]},"afterMinutes":{"type":"integer","description":"Grace after the field's instant. Absent: 0.","minimum":0,"maximum":10080}}},"CollectionLabel":{"type":"object","additionalProperties":false,"required":["singular","plural"],"properties":{"singular":{"type":"string","minLength":1,"maxLength":100},"plural":{"type":"string","minLength":1,"maxLength":100},"description":{"type":"string","maxLength":1000}}},"CollectionSettings":{"type":"object","additionalProperties":false,"properties":{"permalink":{"type":"string","description":"RFC-0038: where the collection's entries live, as a template with a closed token set. Version 1: one or more fixed lower-case path segments followed by /{slug}, e.g. /blog/{slug}. Absent means //{slug}.","pattern":"^(/[a-z0-9]+(-[a-z0-9]+)*)+/\\{slug\\}$"},"labels":{"type":"object","description":"RFC-0040: display names per locale (Studio, agents, themes). Absent or missing a locale: the default locale's, then the key.","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"$ref":"#/$defs/CollectionLabel"}},"icon":{"type":"string","description":"RFC-0040: a name from Studio's closed icon set (e.g. calendar, briefcase, users). Unknown names show the default icon. Never a URL or markup.","pattern":"^[a-z0-9]+(-[a-z0-9]+)*$","maxLength":40},"feed":{"type":"boolean","description":"RFC-0040: entries appear in the site's RSS feeds. Absent: true."},"sitemap":{"type":"boolean","description":"RFC-0040: published entries appear in sitemap.xml. Absent: true."},"sort":{"type":"string","description":"RFC-0040: default order where none is given (Studio's list, entry queries without sort, MCP list_entries). Absent: newest.","enum":["newest","oldest","title"]},"fields":{"type":"array","description":"RFC-0041: the collection's typed custom fields, in form order. Values live in ContentEntry.fields under each key. At most 200 per site and unique keys (checked in code).","minItems":1,"maxItems":30,"items":{"$ref":"#/$defs/FieldDefinition"}},"listFields":{"type":"array","description":"RFC-0042: fields that listings, queries, Studio columns and MCP may filter and sort by, in column order. Keys of this collection's fields (checked in code).","minItems":1,"maxItems":4,"uniqueItems":true,"items":{"$ref":"#/$defs/FieldKey"}},"expiry":{"$ref":"#/$defs/CollectionExpiry"},"structuredData":{"$ref":"#/$defs/StructuredDataPreset"}}},"Consent":{"type":"object","additionalProperties":false,"required":["mode"],"properties":{"mode":{"type":"string","enum":["none","notice","choices"]},"policy":{"$ref":"#/$defs/Path"}}},"Design":{"type":"object","description":"The site's design rules for people and agents (RFC-0021): Markdown that agents read with the theme's DESIGN.md before they build. Guidance only: it changes neither rendering nor permissions.","additionalProperties":false,"required":["markdown"],"properties":{"markdown":{"type":"string","minLength":1,"maxLength":24000}}},"Domain":{"type":"object","additionalProperties":false,"required":["hostname","primary"],"properties":{"hostname":{"type":"string","pattern":"^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,63}$","maxLength":253},"primary":{"type":"boolean"}}},"EntryValueProperty":{"type":"string","description":"RFC-0042: entry properties a structured-data property may read.","enum":["title","summary","body","coverImage","url","publishedAt","updatedAt"]},"FieldCondition":{"type":"object","description":"RFC-0042: one condition on a typed field of the listed collection. `value` is read by the field's type: a number, YYYY-MM-DD, an RFC 3339 instant, true/false, an option value, or `now`. `values` only with `in`.","additionalProperties":false,"required":["field","op"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","in","set","unset"]},"value":{"type":"string","minLength":1,"maxLength":100},"values":{"type":"array","minItems":1,"maxItems":20,"uniqueItems":true,"items":{"type":"string","minLength":1,"maxLength":100}}}},"FieldDefinition":{"description":"RFC-0041: one typed custom field of a collection, discriminated by `type`.","oneOf":[{"title":"TextField","type":"object","description":"Plain text: a string of at most maxLength characters.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["text"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"maxLength":{"type":"integer","description":"Absent: 5000.","minimum":1,"maximum":5000},"multiline":{"type":"boolean","description":"Line breaks allowed. Absent: false."},"searchable":{"type":"boolean","description":"Its words go into site search. Absent: false."}}},{"title":"RichTextField","type":"object","description":"Cloudhal Markdown v1 (ADR-0028), at most 50 000 characters.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["richText"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"searchable":{"type":"boolean","description":"Its words go into site search. Absent: false."}}},{"title":"NumberField","type":"object","description":"A finite number.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["number"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"integer":{"type":"boolean","description":"Whole numbers only. Absent: false."},"minimum":{"type":"number"},"maximum":{"type":"number"}}},{"title":"BooleanField","type":"object","description":"true or false; absent reads as false, so it cannot be required to publish.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["boolean"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"DateField","type":"object","description":"A calendar day without a zone (YYYY-MM-DD).","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["date"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"DateTimeField","type":"object","description":"An instant with the zone it was entered in: {at, timeZone} (RFC-0008 §9.1).","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["datetime"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"UrlField","type":"object","description":"A link: https, http, mailto, tel, a site-relative path or a #fragment; at most 2048 characters.","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["url"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"ImageField","type":"object","description":"An image Asset of the same site, by id (asset_…).","additionalProperties":false,"required":["key","type","labels","localized"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["image"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"}}},{"title":"SelectField","type":"object","description":"One of a closed list of options, or several when multiple.","additionalProperties":false,"required":["key","type","labels","localized","options"],"properties":{"key":{"$ref":"#/$defs/FieldKey"},"type":{"type":"string","enum":["select"]},"labels":{"$ref":"#/$defs/FieldLabels"},"localized":{"type":"boolean","description":"true: each language version has its own value (missing reads the original's); false: only the translation group's original holds it."},"requiredToPublish":{"type":"boolean","description":"An entry in review or published needs a value. Absent: false."},"migration":{"$ref":"#/$defs/FieldMigration"},"options":{"type":"array","minItems":1,"maxItems":50,"items":{"$ref":"#/$defs/FieldOption"}},"multiple":{"type":"boolean","description":"The value is a list of option values. Absent: false."}}}]},"FieldKey":{"type":"string","description":"The field's key in ContentEntry.fields: lower camelCase, a JavaScript identifier. Unique in its collection; built-in entry property names are reserved (checked in code).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"FieldLabel":{"type":"object","additionalProperties":false,"required":["label"],"properties":{"label":{"type":"string","minLength":1,"maxLength":100},"help":{"type":"string","maxLength":300}}},"FieldLabels":{"type":"object","description":"The field's label and help per locale. Missing a locale: the default locale's, then the key.","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"$ref":"#/$defs/FieldLabel"}},"FieldMigration":{"type":"object","description":"RFC-0041 §6: present only while a field operation rewrites entries.","additionalProperties":false,"required":["id"],"properties":{"id":{"$ref":"#/$defs/Id","description":"The operation's \"before\" snapshot."},"fromKey":{"$ref":"#/$defs/FieldKey"},"fromType":{"type":"string","enum":["text","richText","number","boolean","date","datetime","url","image","select"]},"fromLocalized":{"type":"boolean"}}},"FieldOption":{"type":"object","description":"One option of a select field. Values are unique in the field (checked in code).","additionalProperties":false,"required":["value","labels"],"properties":{"value":{"$ref":"#/$defs/Slug","description":"Stored in entries, never shown."},"labels":{"type":"object","propertyNames":{"$ref":"#/$defs/Locale"},"minProperties":1,"additionalProperties":{"type":"string","minLength":1,"maxLength":100}}}},"FieldOrder":{"type":"object","description":"RFC-0042: order a listing by a number, date or datetime field. Entries without a value sort last.","additionalProperties":false,"required":["field","direction"],"properties":{"field":{"type":"string","description":"A field key (site.schema.json FieldKey's pattern, repeated).","pattern":"^[a-z][A-Za-z0-9]{0,39}$"},"direction":{"type":"string","enum":["asc","desc"]}}},"FooterColumn":{"type":"object","additionalProperties":false,"required":["heading","menu"],"properties":{"heading":{"type":"string","minLength":1,"maxLength":60},"menu":{"$ref":"#/$defs/Slug","description":"A menu key (RFC-0003)."}}},"HeaderAction":{"type":"object","additionalProperties":false,"required":["kind"],"properties":{"kind":{"type":"string","enum":["button","search","locale","theme-toggle","motion-toggle"]},"label":{"type":"string","minLength":1,"maxLength":40},"href":{"type":"string","minLength":1,"maxLength":2048},"variant":{"type":"string","enum":["primary","secondary"]}}},"Id":{"type":"string","description":"Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.","pattern":"^[a-z]+_[A-Za-z0-9]{6,32}$","minLength":8,"maxLength":48},"ImageRef":{"type":"object","description":"Reference to an image. With `assetId` the image is an Asset node (RFC-0004): the store writes its media URL into `src`, and its size into `width` and `height`, as a fallback for readers that do not resolve assets. Without it, `src` is a relative path or an absolute URL, as before.","additionalProperties":false,"required":["src","alt"],"properties":{"assetId":{"$ref":"#/$defs/Id","description":"The Asset node shown (RFC-0004). Renderers that know assets resolve it and ignore `src`."},"src":{"type":"string","minLength":1,"maxLength":2048},"alt":{"type":"string","maxLength":500},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}},"Link":{"type":"object","additionalProperties":false,"required":["label","href"],"properties":{"label":{"type":"string","minLength":1,"maxLength":200},"href":{"type":"string","minLength":1,"maxLength":2048},"external":{"type":"boolean"}}},"Locale":{"type":"string","description":"BCP 47 language tag, e.g. en, tr, en-US.","pattern":"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$"},"Localization":{"type":"object","description":"How the site serves its locales (RFC-0033 §2). Absent fields mean: prefix-except-default routing, no fallback, no required review, the machine-translation notice until reviewed.","additionalProperties":false,"properties":{"routing":{"type":"string","description":"prefix-except-default: the default locale at the root, the others under //. prefix-all: every locale prefixed, / is a language chooser (x-default).","enum":["prefix-except-default","prefix-all"]},"domains":{"type":"object","description":"Optional hostname per locale; that locale is then served at the host's root.","propertyNames":{"$ref":"#/$defs/Locale"},"additionalProperties":{"type":"string","pattern":"^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,63}$","maxLength":253}},"fallback":{"type":"string","description":"none: a missing translation is not found. default-with-notice: the default locale's page is served with a notice and noindex.","enum":["none","default-with-notice"]},"review":{"type":"object","description":"Collections whose machine translations need a person's review before they publish ('pages' stands for pages).","additionalProperties":false,"properties":{"required":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"uniqueItems":true}}},"label":{"type":"string","description":"When readers see the machine-translation notice: unreviewed (until a person reviews) or always.","enum":["unreviewed","always"]}}},"Metadata":{"type":"object","description":"Free-form string-to-string map for extensions. Keys should be namespaced (e.g. cloudhal.source).","additionalProperties":{"type":"string"}},"Path":{"type":"string","description":"Absolute URL path of a route the site owns: starts with '/', lower-case slug segments, no trailing slash except root.","pattern":"^/(?:[a-z0-9]+(?:-[a-z0-9]+)*(?:/[a-z0-9]+(?:-[a-z0-9]+)*)*)?$","maxLength":1024},"Revision":{"type":"integer","description":"Monotonic per-node revision used for optimistic locking (compare-and-swap on write).","minimum":0},"SchemaVersion":{"type":"integer","description":"Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.","enum":[1]},"Section":{"title":"Section","description":"A block inside a page, discriminated by `kind`. Variants are written inline (not via $ref) and `kind` is a single-value string enum so that TypeScript gets a literal union and Rust gets a serde internally-tagged enum.","oneOf":[{"type":"object","title":"HeroSection","additionalProperties":false,"required":["id","kind","heading"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["hero"]},"heading":{"type":"string","minLength":1,"maxLength":300},"subheading":{"type":"string","maxLength":1000},"image":{"$ref":"#/$defs/ImageRef"},"cta":{"$ref":"#/$defs/Link"}}},{"type":"object","title":"RichTextSection","additionalProperties":false,"required":["id","kind","format","body"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["richText"]},"format":{"type":"string","enum":["markdown","html"]},"body":{"type":"string","maxLength":200000},"profile":{"$ref":"#/$defs/BodyProfile"},"blocks":{"$ref":"#/$defs/BodyBlocks"}}},{"type":"object","title":"ImageSection","additionalProperties":false,"required":["id","kind","image"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["image"]},"image":{"$ref":"#/$defs/ImageRef"},"caption":{"type":"string","maxLength":1000}}},{"type":"object","title":"ContentListSection","description":"Renders the newest entries of a content collection (e.g. blog posts).","additionalProperties":false,"required":["id","kind","collection","limit","sort"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["contentList"]},"collection":{"$ref":"#/$defs/Slug"},"limit":{"type":"integer","minimum":1,"maximum":100},"sort":{"type":"string","enum":["newest","oldest","title"]},"filter":{"type":"string","description":"Filter expression over term assignments (RFC-0002), e.g. 'category:news+tag:ai' or 'category:news>*'.","minLength":1,"maxLength":512},"where":{"type":"array","description":"RFC-0042: conditions on the collection's list fields, ANDed.","minItems":1,"maxItems":4,"items":{"$ref":"#/$defs/FieldCondition"}},"orderBy":{"$ref":"#/$defs/FieldOrder"}}},{"type":"object","title":"ComponentSection","description":"A typed block from the block registry (RFC-0015): `type` names a registered block type (one global namespace of English names), `props` follow that type's props schema, which the store validates.","additionalProperties":false,"required":["id","kind","type","props"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["component"]},"type":{"type":"string","pattern":"^[a-z][a-z0-9-]{1,40}$"},"props":{"type":"object","description":"Validated against the type's props schema by the store, not by this contract.","additionalProperties":true}}},{"type":"object","title":"SurfaceSection","description":"A generative UI surface (RFC-0020): a flat list of catalog components with a `root`, plus a data model their props may bind to. Written and streamed by agents as data, never code; the store checks the tree, the catalog and the limits.","additionalProperties":false,"required":["id","kind","catalog","components"],"properties":{"id":{"$ref":"#/$defs/Id"},"kind":{"type":"string","enum":["surface"]},"catalog":{"type":"string","description":"The catalog the surface was written for: `@` (RFC-0018 manifest).","pattern":"^[a-z][a-z0-9-]{0,40}@[0-9]{1,4}$"},"components":{"type":"array","minItems":1,"maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponent"}},"data":{"type":"object","description":"The surface's data model: JSON its components bind to with `{path}` (at most 64 KB serialized; the store enforces the size).","additionalProperties":true}}}]},"SiteValueProperty":{"type":"string","description":"RFC-0042: site properties a structured-data property may read.","enum":["name","url","logo"]},"Slug":{"type":"string","description":"Lower-case URL segment.","pattern":"^[a-z0-9]+(?:-[a-z0-9]+)*$","minLength":1,"maxLength":128},"SocialLink":{"type":"object","additionalProperties":false,"required":["network","href"],"properties":{"network":{"type":"string","enum":["x","bluesky","mastodon","github","linkedin","instagram","facebook","youtube","tiktok","threads","rss","email"]},"href":{"type":"string","minLength":1,"maxLength":2048}}},"StructuredDataPreset":{"type":"object","description":"RFC-0042: the JSON-LD type an entry page emits in place of BlogPosting, with its properties mapped to value sources. Paths, fitting field types and required properties are a closed table in code (@cloudhal/core-ts/structured-data).","additionalProperties":false,"required":["type","properties"],"properties":{"type":{"type":"string","enum":["Event","JobPosting","Product","Recipe"]},"properties":{"type":"object","description":"Keyed by property path, e.g. startDate or location.address.addressLocality.","propertyNames":{"type":"string","pattern":"^[a-z][A-Za-z]{0,39}(\\.[a-z][A-Za-z]{0,39}){0,2}$"},"minProperties":1,"maxProperties":40,"additionalProperties":{"$ref":"#/$defs/StructuredDataSource"}}}},"StructuredDataSource":{"description":"RFC-0042: where one JSON-LD property's value comes from, discriminated by `kind`.","oneOf":[{"title":"FieldValueSource","type":"object","description":"A typed field of this collection, converted by its type.","additionalProperties":false,"required":["kind","field"],"properties":{"kind":{"type":"string","enum":["field"]},"field":{"$ref":"#/$defs/FieldKey"}}},{"title":"EntryValueSource","type":"object","description":"A built-in property of the entry.","additionalProperties":false,"required":["kind","property"],"properties":{"kind":{"type":"string","enum":["entry"]},"property":{"$ref":"#/$defs/EntryValueProperty"}}},{"title":"SiteValueSource","type":"object","description":"A property of the site.","additionalProperties":false,"required":["kind","property"],"properties":{"kind":{"type":"string","enum":["site"]},"property":{"$ref":"#/$defs/SiteValueProperty"}}},{"title":"ConstantValueSource","type":"object","description":"A plain-text constant: no control characters, no markup meaning; vocabulary-checked on enum paths (in code).","additionalProperties":false,"required":["kind","value"],"properties":{"kind":{"type":"string","enum":["constant"]},"value":{"type":"string","minLength":1,"maxLength":200,"pattern":"^[^\\x00-\\x1F\\x7F]+$"}}}]},"SurfaceComponent":{"type":"object","description":"One element of a generative UI surface (RFC-0020 §1), in a flat list: `component` names a catalog entry (a PascalCase built-in primitive such as `Stack`, or a registry block type such as `callout`), `children` refer to other ids of the same surface, and `props` follow the entry's schema, which the store checks. Field names follow A2UI v0.9.","additionalProperties":false,"required":["id","component"],"properties":{"id":{"$ref":"#/$defs/SurfaceComponentId"},"component":{"type":"string","pattern":"^(?:[A-Z][A-Za-z0-9]{1,40}|[a-z][a-z0-9-]{1,40})$"},"children":{"type":"array","maxItems":200,"items":{"$ref":"#/$defs/SurfaceComponentId"}},"props":{"type":"object","description":"Literal values, `{path}` bindings into the surface's data, or `{call}` named functions; validated by the store against the catalog, not by this contract.","additionalProperties":true},"visible":{"type":"object","description":"A visibility condition over the surface's data (`eq`, `ne`, `gt`, `lt`, `and`, `or`, `not`); validated by the store.","additionalProperties":true},"motion":{"type":"object","description":"Motion presets for this component (RFC-0024 §2): `reveal`, `hover`, `speed`, `delay`, `stagger`, `replay` and later `scroll`, `loop`; enums and clamped numbers only, validated by the store against the catalog. The renderer turns them into CSS; agents never write CSS.","additionalProperties":true}}},"SurfaceComponentId":{"type":"string","description":"A component's id, unique within its surface; `root` is the tree's root (RFC-0020 §1).","pattern":"^[a-z][a-z0-9-]{0,40}$"},"ThemeRef":{"type":"object","description":"Theme (a.k.a. Experience) applied by the renderer. Config is theme-defined and validated by the theme's own schema, not here.","additionalProperties":false,"required":["name"],"properties":{"name":{"$ref":"#/$defs/Slug"},"version":{"type":"string","maxLength":64},"config":{"type":"object","additionalProperties":true}}},"Timestamp":{"type":"string","description":"RFC 3339 / ISO 8601 instant in UTC.","format":"date-time"},"TimeZone":{"type":"string","description":"IANA time zone name from the tz database (RFC-0008), e.g. Europe/Istanbul, America/Argentina/Buenos_Aires, Etc/GMT+3 or UTC. Wall-clock times a person enters are kept with their zone, so the instant is recomputed when governments change the rules. The pattern bounds the shape only; writers also check the name against the runtime's Intl zone list.","pattern":"^(?:UTC|[A-Z][A-Za-z_-]+(?:/[A-Za-z0-9_+-]+){1,2})$","maxLength":64}}}; const 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}; @@ -3187,7 +3187,7 @@ validate49.evaluated = {"props":true,"dynamicProps":false,"dynamicItems":false}; const schema79 = {"type":"object","description":"RFC-0042: the JSON-LD type an entry page emits in place of BlogPosting, with its properties mapped to value sources. Paths, fitting field types and required properties are a closed table in code (@cloudhal/core-ts/structured-data).","additionalProperties":false,"required":["type","properties"],"properties":{"type":{"type":"string","enum":["Event","JobPosting","Product","Recipe"]},"properties":{"type":"object","description":"Keyed by property path, e.g. startDate or location.address.addressLocality.","propertyNames":{"type":"string","pattern":"^[a-z][A-Za-z]{0,39}(\\.[a-z][A-Za-z]{0,39}){0,2}$"},"minProperties":1,"maxProperties":40,"additionalProperties":{"$ref":"#/$defs/StructuredDataSource"}}}}; const pattern38 = new RegExp("^[a-z][A-Za-z]{0,39}(\\.[a-z][A-Za-z]{0,39}){0,2}$", "u"); -const schema80 = {"description":"RFC-0042: where one JSON-LD property's value comes from, discriminated by `kind`.","oneOf":[{"title":"FieldValueSource","type":"object","description":"A typed field of this collection, converted by its type.","additionalProperties":false,"required":["kind","field"],"properties":{"kind":{"type":"string","enum":["field"]},"field":{"$ref":"#/$defs/FieldKey"}}},{"title":"EntryValueSource","type":"object","description":"A built-in property of the entry.","additionalProperties":false,"required":["kind","property"],"properties":{"kind":{"type":"string","enum":["entry"]},"property":{"$ref":"#/$defs/EntryValueProperty"}}},{"title":"SiteValueSource","type":"object","description":"A property of the site.","additionalProperties":false,"required":["kind","property"],"properties":{"kind":{"type":"string","enum":["site"]},"property":{"$ref":"#/$defs/SiteValueProperty"}}},{"title":"TextValueSource","type":"object","description":"A plain-text constant: no control characters, no markup meaning; vocabulary-checked on enum paths (in code).","additionalProperties":false,"required":["kind","value"],"properties":{"kind":{"type":"string","enum":["text"]},"value":{"type":"string","minLength":1,"maxLength":200,"pattern":"^[^\\x00-\\x1F\\x7F]+$"}}}]}; +const schema80 = {"description":"RFC-0042: where one JSON-LD property's value comes from, discriminated by `kind`.","oneOf":[{"title":"FieldValueSource","type":"object","description":"A typed field of this collection, converted by its type.","additionalProperties":false,"required":["kind","field"],"properties":{"kind":{"type":"string","enum":["field"]},"field":{"$ref":"#/$defs/FieldKey"}}},{"title":"EntryValueSource","type":"object","description":"A built-in property of the entry.","additionalProperties":false,"required":["kind","property"],"properties":{"kind":{"type":"string","enum":["entry"]},"property":{"$ref":"#/$defs/EntryValueProperty"}}},{"title":"SiteValueSource","type":"object","description":"A property of the site.","additionalProperties":false,"required":["kind","property"],"properties":{"kind":{"type":"string","enum":["site"]},"property":{"$ref":"#/$defs/SiteValueProperty"}}},{"title":"ConstantValueSource","type":"object","description":"A plain-text constant: no control characters, no markup meaning; vocabulary-checked on enum paths (in code).","additionalProperties":false,"required":["kind","value"],"properties":{"kind":{"type":"string","enum":["constant"]},"value":{"type":"string","minLength":1,"maxLength":200,"pattern":"^[^\\x00-\\x1F\\x7F]+$"}}}]}; const schema82 = {"type":"string","description":"RFC-0042: entry properties a structured-data property may read.","enum":["title","summary","body","coverImage","url","publishedAt","updatedAt"]}; const schema83 = {"type":"string","description":"RFC-0042: site properties a structured-data property may read.","enum":["name","url","logo"]}; const pattern40 = new RegExp("^[^\\x00-\\x1F\\x7F]+$", "u"); @@ -3558,7 +3558,7 @@ vErrors.push(err27); } errors++; } -if(!(data6 === "text")){ +if(!(data6 === "constant")){ const err28 = {instancePath:instancePath+"/kind",schemaPath:"#/oneOf/3/properties/kind/enum",keyword:"enum",params:{allowedValues: schema80.oneOf[3].properties.kind.enum},message:"must be equal to one of the allowed values"}; if(vErrors === null){ vErrors = [err28]; diff --git a/packages/core-ts/src/structured-data/check.ts b/packages/core-ts/src/structured-data/check.ts index 7e98b745..e7531206 100644 --- a/packages/core-ts/src/structured-data/check.ts +++ b/packages/core-ts/src/structured-data/check.ts @@ -143,7 +143,7 @@ function checkSource(ctx: PathContext, source: StructuredDataSource): Structured `${path} does not take the site's ${source.property}`, ), ]; - case "text": + case "constant": return checkConstant(ctx, source.value); default: return [err(at, "source_mismatch", "an unknown source")]; @@ -168,7 +168,7 @@ function checkRequired(preset: StructuredDataPreset, at: string): StructuredData if (remote) continue; const fromEntry = group.some((p) => { const s = preset.properties[p]; - return s !== undefined && s.kind !== "text"; + return s !== undefined && s.kind !== "constant"; }); if (!fromEntry) { const prefix = group[0]?.replace(/\.[^.]+$/, "") ?? ""; diff --git a/packages/core-ts/src/structured-data/values.ts b/packages/core-ts/src/structured-data/values.ts index f5625ff3..abb06e02 100644 --- a/packages/core-ts/src/structured-data/values.ts +++ b/packages/core-ts/src/structured-data/values.ts @@ -260,7 +260,7 @@ export function sourceValue( return fromEntry(source.property, input); case "site": return fromSite(source.property, input); - case "text": + case "constant": return fromConstant(source.value, input.spec); default: return undefined; diff --git a/packages/core-ts/test/structured-data.test.ts b/packages/core-ts/test/structured-data.test.ts index 6ddd38d4..3a231d32 100644 --- a/packages/core-ts/test/structured-data.test.ts +++ b/packages/core-ts/test/structured-data.test.ts @@ -149,12 +149,12 @@ describe("checkStructuredData (save time)", () => { it("refuses paths outside the type's table", () => { const p = { ...EVENT, - properties: { ...EVENT.properties, aggregateRating: { kind: "text", value: "5" } }, + properties: { ...EVENT.properties, aggregateRating: { kind: "constant", value: "5" } }, }; expect(codes(p as StructuredDataPreset)).toEqual(["unknown_path properties/aggregateRating"]); const proto = { ...EVENT, - properties: { ...EVENT.properties, toString: { kind: "text", value: "x" } }, + properties: { ...EVENT.properties, toString: { kind: "constant", value: "x" } }, }; expect(codes(proto as StructuredDataPreset)).toEqual(["unknown_path properties/toString"]); }); @@ -196,7 +196,7 @@ describe("checkStructuredData (save time)", () => { const p = (value: string, path: string) => codes({ ...EVENT, - properties: { ...EVENT.properties, [path]: { kind: "text", value } }, + properties: { ...EVENT.properties, [path]: { kind: "constant", value } }, } as StructuredDataPreset); expect(p("EUR", "offers.priceCurrency")).toEqual([]); expect(p("eur", "offers.priceCurrency")).toEqual([ @@ -246,15 +246,15 @@ describe("checkStructuredData (save time)", () => { type: "Event", properties: { ...EVENT.properties, - "location.address.addressLocality": { kind: "text", value: "Istanbul" }, + "location.address.addressLocality": { kind: "constant", value: "Istanbul" }, }, }), ).toEqual(["required_missing properties/location.address"]); - expect(codes({ type: "Product", properties: { sku: { kind: "text", value: "A1" } } })).toEqual([ + expect(codes({ type: "Product", properties: { sku: { kind: "constant", value: "A1" } } })).toEqual([ "required_missing properties/offers.price", ]); expect( - codes({ type: "Recipe", properties: { keywords: { kind: "text", value: "soup" } } }), + codes({ type: "Recipe", properties: { keywords: { kind: "constant", value: "soup" } } }), ).toEqual([]); }); @@ -267,9 +267,9 @@ describe("checkStructuredData (save time)", () => { type: "JobPosting", properties: { jobLocationType: { kind: "field", field: "remote" }, - "applicantLocationRequirements.name": { kind: "text", value: "Türkiye" }, + "applicantLocationRequirements.name": { kind: "constant", value: "Türkiye" }, employmentType: { kind: "field", field: "kinds" }, - "baseSalary.unitText": { kind: "text", value: "MONTH" }, + "baseSalary.unitText": { kind: "constant", value: "MONTH" }, }, }), ).toEqual([]); @@ -328,9 +328,9 @@ describe("structuredDataFor (per entry)", () => { ...EVENT.properties, endDate: { kind: "field", field: "day" }, eventStatus: { kind: "field", field: "status" }, - "location.address.addressCountry": { kind: "text", value: "TR" }, + "location.address.addressCountry": { kind: "constant", value: "TR" }, "offers.price": { kind: "field", field: "price" }, - "offers.priceCurrency": { kind: "text", value: "TRY" }, + "offers.priceCurrency": { kind: "constant", value: "TRY" }, "offers.url": { kind: "field", field: "tickets" }, "organizer.name": { kind: "site", property: "name" }, "organizer.url": { kind: "site", property: "url" }, @@ -416,11 +416,11 @@ describe("structuredDataFor (per entry)", () => { properties: { validThrough: { kind: "field", field: "day" }, jobLocationType: { kind: "field", field: "remote" }, - "applicantLocationRequirements.name": { kind: "text", value: "Türkiye" }, + "applicantLocationRequirements.name": { kind: "constant", value: "Türkiye" }, employmentType: { kind: "field", field: "kinds" }, "baseSalary.value": { kind: "field", field: "price" }, - "baseSalary.currency": { kind: "text", value: "EUR" }, - "baseSalary.unitText": { kind: "text", value: "month" }, + "baseSalary.currency": { kind: "constant", value: "EUR" }, + "baseSalary.unitText": { kind: "constant", value: "month" }, directApply: { kind: "field", field: "remote" }, identifier: { kind: "field", field: "venue" }, }, @@ -507,7 +507,7 @@ describe("structuredDataFor (per entry)", () => { properties: { "offers.price": { kind: "field", field: "price" }, "offers.priceCurrency": { kind: "field", field: "venue" }, - "offers.availability": { kind: "text", value: "in-stock" }, + "offers.availability": { kind: "constant", value: "in-stock" }, "offers.priceValidUntil": { kind: "entry", property: "updatedAt" }, }, }; From 3b55f8fd61237f5b33ef17b1abeb06e7822f1a4f Mon Sep 17 00:00:00 2001 From: Madraka <8898640+Madraka@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:14:06 +0300 Subject: [PATCH 3/3] style: format structured-data test Co-Authored-By: Claude Opus 5.5 Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com> --- packages/core-ts/test/structured-data.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core-ts/test/structured-data.test.ts b/packages/core-ts/test/structured-data.test.ts index 3a231d32..75c45cc8 100644 --- a/packages/core-ts/test/structured-data.test.ts +++ b/packages/core-ts/test/structured-data.test.ts @@ -250,9 +250,9 @@ describe("checkStructuredData (save time)", () => { }, }), ).toEqual(["required_missing properties/location.address"]); - expect(codes({ type: "Product", properties: { sku: { kind: "constant", value: "A1" } } })).toEqual([ - "required_missing properties/offers.price", - ]); + expect( + codes({ type: "Product", properties: { sku: { kind: "constant", value: "A1" } } }), + ).toEqual(["required_missing properties/offers.price"]); expect( codes({ type: "Recipe", properties: { keywords: { kind: "constant", value: "soup" } } }), ).toEqual([]);