diff --git a/.changeset/validation-rules-cadence-operator.md b/.changeset/validation-rules-cadence-operator.md new file mode 100644 index 000000000..4837ee3b5 --- /dev/null +++ b/.changeset/validation-rules-cadence-operator.md @@ -0,0 +1,6 @@ +--- +"@epilot/validation-rules-client": minor +"@epilot/sdk": minor +--- + +Refresh the Validation Rules API contract: adds the `maxPerPeriod` operator and the `CadenceValue` condition value that limits how often something may happen within a period, counting records of a bound context type (e.g. tickets). diff --git a/clients/validation-rules-client/package.json b/clients/validation-rules-client/package.json index 02690c93c..eca6ddf3b 100644 --- a/clients/validation-rules-client/package.json +++ b/clients/validation-rules-client/package.json @@ -1,6 +1,6 @@ { "name": "@epilot/validation-rules-client", - "version": "1.6.0-rc.1", + "version": "1.7.0-rc.0", "description": "API Client for epilot Validation Rules API", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/clients/validation-rules-client/src/openapi.d.ts b/clients/validation-rules-client/src/openapi.d.ts index 74e342abb..bdabd407c 100644 --- a/clients/validation-rules-client/src/openapi.d.ts +++ b/clients/validation-rules-client/src/openapi.d.ts @@ -53,6 +53,42 @@ declare namespace Components { */ EnvironmentValue; } + /** + * The value of a `maxPerPeriod` condition: what is counted and how many are allowed within the + * period ending at the evaluation moment. The rule stores the settings only; the consumer resolves + * the counted records (e.g. the tickets bound to the rule's `ticket` context) and supplies their + * timestamps at evaluation time. + * + * `count` says what kind of thing is counted. Today only `records` exists: entities of the + * context type named by the first segment of `path`, dated by the attribute in the rest of the + * path (e.g. `ticket._created_at`). Further kinds may be added later without affecting existing + * rules. + * + * Rolling units (`days`, `weeks`, `months`) count back from now. Calendar units + * (`calendar_weeks`, `calendar_months`) start at the beginning of the current calendar week + * (Monday) or month, extended backwards by `period - 1` units. + * + */ + export interface CadenceValue { + source: "cadence"; + /** + * What is counted. `records` counts entities of the context type in `path`, dated by the attribute in `path`. + */ + count: "records"; + /** + * The counted records as `.`, e.g. `ticket._created_at`. + */ + path: string; // ^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z0-9_]+)*$ + /** + * How many counted records may already fall inside the period before the condition fails. 0 never allows another one. + */ + max: number; + /** + * Length of the period in `unit`s. + */ + period: number; + unit: "days" | "weeks" | "months" | "calendar_weeks" | "calendar_months"; + } /** * Declarative validation rule (schema version v2). Supports predefined comparison operators * over number, date and text inputs, with static, dynamic (context path), relative-date and @@ -96,10 +132,13 @@ declare namespace Components { * separators, sign and the decimal separator are not counted); maxDecimals limits how many * digits may follow the decimal separator. Both take a non-negative integer comparison value * and, like the other numeric operators, are also allowed on text rules. + * maxPerPeriod is allowed on every input type and requires a `cadence` value. It judges how often + * something happened rather than the input itself: the condition holds while fewer than `max` + * counted records of a context entity type fall inside the period ending now. * */ Operator; - value: /* The comparison value of a condition - a scalar, a range of scalars, or nothing (unary operators). */ ConditionValue; + value: /* The comparison value of a condition - a scalar, a range of scalars, a cadence (maxPerPeriod), or nothing (unary operators). */ ConditionValue; /** * Message shown to the end user when this condition fails. */ @@ -122,9 +161,9 @@ declare namespace Components { allow_failure?: boolean; } /** - * The comparison value of a condition - a scalar, a range of scalars, or nothing (unary operators). + * The comparison value of a condition - a scalar, a range of scalars, a cadence (maxPerPeriod), or nothing (unary operators). */ - export type ConditionValue = /* The comparison value of a condition - a scalar, a range of scalars, or nothing (unary operators). */ /* A fixed comparison value. */ StaticValue | /** + export type ConditionValue = /* The comparison value of a condition - a scalar, a range of scalars, a cadence (maxPerPeriod), or nothing (unary operators). */ /* A fixed comparison value. */ StaticValue | /** * A dynamic comparison value resolved from runtime context, e.g. `contract.installment_amount` * or `previous_reading.value`. The first path segment must match the `name` of a declared * context requirement. @@ -156,10 +195,27 @@ declare namespace Components { * in public journeys the dependent conditions are skipped. * */ - ExternalValue | /* A lower and upper bound for range operators (between, dateBetween, lengthBetween). Bounds are inclusive. */ RangeValue | /* No comparison value - used by unary operators such as notInFuture / notInPast. */ NoValue; + ExternalValue | /* A lower and upper bound for range operators (between, dateBetween, lengthBetween). Bounds are inclusive. */ RangeValue | /** + * The value of a `maxPerPeriod` condition: what is counted and how many are allowed within the + * period ending at the evaluation moment. The rule stores the settings only; the consumer resolves + * the counted records (e.g. the tickets bound to the rule's `ticket` context) and supplies their + * timestamps at evaluation time. + * + * `count` says what kind of thing is counted. Today only `records` exists: entities of the + * context type named by the first segment of `path`, dated by the attribute in the rest of the + * path (e.g. `ticket._created_at`). Further kinds may be added later without affecting existing + * rules. + * + * Rolling units (`days`, `weeks`, `months`) count back from now. Calendar units + * (`calendar_weeks`, `calendar_months`) start at the beginning of the current calendar week + * (Monday) or month, extended backwards by `period - 1` units. + * + */ + CadenceValue | /* No comparison value - used by unary operators such as notInFuture / notInPast. */ NoValue; /** * An entity context source the rule needs at evaluation time, referenced by `context` - * value paths via the schema slug as their first segment (e.g. `contract.installment_amount`). + * value paths (e.g. `contract.installment_amount`) and `cadence` value paths (e.g. `ticket._created_at`) + * via the schema slug as their first segment. * How the source is resolved (which entity instance) is decided by the consuming surface, * not by the rule. Meter reading comparisons use the meter/meter_counter entity schemas * (e.g. `meter_counter.current_consumption` for the previous reading value). @@ -241,7 +297,8 @@ declare namespace Components { */ contexts?: /** * An entity context source the rule needs at evaluation time, referenced by `context` - * value paths via the schema slug as their first segment (e.g. `contract.installment_amount`). + * value paths (e.g. `contract.installment_amount`) and `cadence` value paths (e.g. `ticket._created_at`) + * via the schema slug as their first segment. * How the source is resolved (which entity instance) is decided by the consuming surface, * not by the rule. Meter reading comparisons use the meter/meter_counter entity schemas * (e.g. `meter_counter.current_consumption` for the previous reading value). @@ -581,9 +638,12 @@ declare namespace Components { * separators, sign and the decimal separator are not counted); maxDecimals limits how many * digits may follow the decimal separator. Both take a non-negative integer comparison value * and, like the other numeric operators, are also allowed on text rules. + * maxPerPeriod is allowed on every input type and requires a `cadence` value. It judges how often + * something happened rather than the input itself: the condition holds while fewer than `max` + * counted records of a context entity type fall inside the period ending now. * */ - export type Operator = "equal" | "notEqual" | "greaterThan" | "greaterThanInclusive" | "lessThan" | "lessThanInclusive" | "between" | "dateBefore" | "dateOnOrBefore" | "dateAfter" | "dateOnOrAfter" | "dateBetween" | "notInFuture" | "notInPast" | "contains" | "doesNotContain" | "startsWith" | "endsWith" | "regexMatch" | "lengthBetween" | "maxDigits" | "maxDecimals"; + export type Operator = "equal" | "notEqual" | "greaterThan" | "greaterThanInclusive" | "lessThan" | "lessThanInclusive" | "between" | "dateBefore" | "dateOnOrBefore" | "dateAfter" | "dateOnOrAfter" | "dateBetween" | "notInFuture" | "notInPast" | "contains" | "doesNotContain" | "startsWith" | "endsWith" | "regexMatch" | "lengthBetween" | "maxDigits" | "maxDecimals" | "maxPerPeriod"; /** * Condition definition for a pattern-based validation rule (2 levels deep) */ @@ -915,7 +975,8 @@ declare namespace Components { */ contexts?: /** * An entity context source the rule needs at evaluation time, referenced by `context` - * value paths via the schema slug as their first segment (e.g. `contract.installment_amount`). + * value paths (e.g. `contract.installment_amount`) and `cadence` value paths (e.g. `ticket._created_at`) + * via the schema slug as their first segment. * How the source is resolved (which entity instance) is decided by the consuming surface, * not by the rule. Meter reading comparisons use the meter/meter_counter entity schemas * (e.g. `meter_counter.current_consumption` for the previous reading value). @@ -1003,7 +1064,8 @@ declare namespace Components { */ contexts?: /** * An entity context source the rule needs at evaluation time, referenced by `context` - * value paths via the schema slug as their first segment (e.g. `contract.installment_amount`). + * value paths (e.g. `contract.installment_amount`) and `cadence` value paths (e.g. `ticket._created_at`) + * via the schema slug as their first segment. * How the source is resolved (which entity instance) is decided by the consuming surface, * not by the rule. Meter reading comparisons use the meter/meter_counter entity schemas * (e.g. `meter_counter.current_consumption` for the previous reading value). @@ -1088,7 +1150,8 @@ declare namespace Components { */ contexts?: /** * An entity context source the rule needs at evaluation time, referenced by `context` - * value paths via the schema slug as their first segment (e.g. `contract.installment_amount`). + * value paths (e.g. `contract.installment_amount`) and `cadence` value paths (e.g. `ticket._created_at`) + * via the schema slug as their first segment. * How the source is resolved (which entity instance) is decided by the consuming surface, * not by the rule. Meter reading comparisons use the meter/meter_counter entity schemas * (e.g. `meter_counter.current_consumption` for the previous reading value). @@ -1628,6 +1691,7 @@ export type Client = OpenAPIClient export type AppliesWhen = Components.Schemas.AppliesWhen; +export type CadenceValue = Components.Schemas.CadenceValue; export type ComparisonRuleType = Components.Schemas.ComparisonRuleType; export type Condition = Components.Schemas.Condition; export type ConditionValue = Components.Schemas.ConditionValue; diff --git a/clients/validation-rules-client/src/openapi.json b/clients/validation-rules-client/src/openapi.json index 56949f420..672923859 100644 --- a/clients/validation-rules-client/src/openapi.json +++ b/clients/validation-rules-client/src/openapi.json @@ -2071,7 +2071,7 @@ }, "Operator": { "type": "string", - "description": "Predefined comparison operator. Compatibility (enforced at write time):\n- number: equal, notEqual, greaterThan, greaterThanInclusive, lessThan, lessThanInclusive, between, regexMatch,\n maxDigits, maxDecimals\n- date: dateBefore, dateOnOrBefore, dateAfter, dateOnOrAfter, dateBetween, notInFuture, notInPast, regexMatch\n- text: equal, notEqual, contains, doesNotContain, startsWith, endsWith, regexMatch, lengthBetween,\n greaterThan, greaterThanInclusive, lessThan, lessThanInclusive, between, maxDigits, maxDecimals\nRange operators (between, dateBetween, lengthBetween) require a `range` value;\nunary operators (notInFuture, notInPast) require a `none` value; all others require a scalar value.\nregexMatch validates the raw input string's format and always takes a static string pattern.\nNumeric comparison operators on text rules parse the input as a number at evaluation time\n(free-text fields often hold numbers); unparsable input fails the condition.\nmaxDigits limits how many digits the written input may contain in total (grouping\nseparators, sign and the decimal separator are not counted); maxDecimals limits how many\ndigits may follow the decimal separator. Both take a non-negative integer comparison value\nand, like the other numeric operators, are also allowed on text rules.\n", + "description": "Predefined comparison operator. Compatibility (enforced at write time):\n- number: equal, notEqual, greaterThan, greaterThanInclusive, lessThan, lessThanInclusive, between, regexMatch,\n maxDigits, maxDecimals\n- date: dateBefore, dateOnOrBefore, dateAfter, dateOnOrAfter, dateBetween, notInFuture, notInPast, regexMatch\n- text: equal, notEqual, contains, doesNotContain, startsWith, endsWith, regexMatch, lengthBetween,\n greaterThan, greaterThanInclusive, lessThan, lessThanInclusive, between, maxDigits, maxDecimals\nRange operators (between, dateBetween, lengthBetween) require a `range` value;\nunary operators (notInFuture, notInPast) require a `none` value; all others require a scalar value.\nregexMatch validates the raw input string's format and always takes a static string pattern.\nNumeric comparison operators on text rules parse the input as a number at evaluation time\n(free-text fields often hold numbers); unparsable input fails the condition.\nmaxDigits limits how many digits the written input may contain in total (grouping\nseparators, sign and the decimal separator are not counted); maxDecimals limits how many\ndigits may follow the decimal separator. Both take a non-negative integer comparison value\nand, like the other numeric operators, are also allowed on text rules.\nmaxPerPeriod is allowed on every input type and requires a `cadence` value. It judges how often\nsomething happened rather than the input itself: the condition holds while fewer than `max`\ncounted records of a context entity type fall inside the period ending now.\n", "enum": [ "equal", "notEqual", @@ -2094,11 +2094,12 @@ "regexMatch", "lengthBetween", "maxDigits", - "maxDecimals" + "maxDecimals", + "maxPerPeriod" ] }, "ConditionValue": { - "description": "The comparison value of a condition - a scalar, a range of scalars, or nothing (unary operators).", + "description": "The comparison value of a condition - a scalar, a range of scalars, a cadence (maxPerPeriod), or nothing (unary operators).", "oneOf": [ { "$ref": "#/components/schemas/StaticValue" @@ -2118,6 +2119,9 @@ { "$ref": "#/components/schemas/RangeValue" }, + { + "$ref": "#/components/schemas/CadenceValue" + }, { "$ref": "#/components/schemas/NoValue" } @@ -2313,7 +2317,7 @@ "up", "down" ], - "description": "Rounds the adjusted result to a whole number - `up` (ceiling) or `down` (floor).\nOmitted means no rounding. Used for bounds like \"round up(current \u00d7 0.9) to whole euros\".\n" + "description": "Rounds the adjusted result to a whole number - `up` (ceiling) or `down` (floor).\nOmitted means no rounding. Used for bounds like \"round up(current × 0.9) to whole euros\".\n" } } }, @@ -2396,6 +2400,61 @@ } } }, + "CadenceValue": { + "type": "object", + "description": "The value of a `maxPerPeriod` condition: what is counted and how many are allowed within the\nperiod ending at the evaluation moment. The rule stores the settings only; the consumer resolves\nthe counted records (e.g. the tickets bound to the rule's `ticket` context) and supplies their\ntimestamps at evaluation time.\n\n`count` says what kind of thing is counted. Today only `records` exists: entities of the\ncontext type named by the first segment of `path`, dated by the attribute in the rest of the\npath (e.g. `ticket._created_at`). Further kinds may be added later without affecting existing\nrules.\n\nRolling units (`days`, `weeks`, `months`) count back from now. Calendar units\n(`calendar_weeks`, `calendar_months`) start at the beginning of the current calendar week\n(Monday) or month, extended backwards by `period - 1` units.\n", + "required": [ + "source", + "count", + "path", + "max", + "period", + "unit" + ], + "additionalProperties": false, + "properties": { + "source": { + "type": "string", + "enum": [ + "cadence" + ] + }, + "count": { + "type": "string", + "enum": [ + "records" + ], + "description": "What is counted. `records` counts entities of the context type in `path`, dated by the attribute in `path`." + }, + "path": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z0-9_]+)*$", + "description": "The counted records as `.`, e.g. `ticket._created_at`." + }, + "max": { + "type": "integer", + "minimum": 0, + "description": "How many counted records may already fall inside the period before the condition fails. 0 never allows another one." + }, + "period": { + "type": "integer", + "minimum": 1, + "description": "Length of the period in `unit`s." + }, + "unit": { + "type": "string", + "enum": [ + "days", + "weeks", + "months", + "calendar_weeks", + "calendar_months" + ] + } + } + }, "DocumentRuleType": { "type": "object", "description": "Declarative validation rule (schema version v2) for uploaded files. The rule declares how deep\nthe check goes (`check.level`) and the conditions the file must satisfy. Property conditions\n(file type, size, page count, image resolution, blank and password-protected detection) are\ndeterministic and available at every level; the `meetsCriteria` condition judges a free-text\nrequirement against the document's extracted content and is available from level `standard`.\n\nEvaluation semantics for content conditions differ from scalar rules: each condition resolves to\npass, fail or uncertain. A confident fail on a blocking condition fails the input; an uncertain\nresult is always reported as advisory and never fails the input, regardless of `allow_failure`.\nDocument rules carry no `contexts` yet.\n", @@ -2618,7 +2677,7 @@ }, "ContextRequirement": { "type": "object", - "description": "An entity context source the rule needs at evaluation time, referenced by `context`\nvalue paths via the schema slug as their first segment (e.g. `contract.installment_amount`).\nHow the source is resolved (which entity instance) is decided by the consuming surface,\nnot by the rule. Meter reading comparisons use the meter/meter_counter entity schemas\n(e.g. `meter_counter.current_consumption` for the previous reading value).\n", + "description": "An entity context source the rule needs at evaluation time, referenced by `context`\nvalue paths (e.g. `contract.installment_amount`) and `cadence` value paths (e.g. `ticket._created_at`)\nvia the schema slug as their first segment.\nHow the source is resolved (which entity instance) is decided by the consuming surface,\nnot by the rule. Meter reading comparisons use the meter/meter_counter entity schemas\n(e.g. `meter_counter.current_consumption` for the previous reading value).\n", "required": [ "schema" ],