Pulse Policy DSL — Grammar
Версия: v0.1 · Синхронизация: spec v0.14.5 · Последнее обновление: 2026-05-20
Формальная грамматика языка выражений Pulse Policy DSL. Покрывает applies_when в правилах, definition в cohorts, и эквивалентные expression-only points в action condition блоках (M2+).
Грамматика стабильна для M1 implementation /dsl/validate endpoint. Изменения требуют bump dsl_grammar_version в meta-schema rules table — это breaking change для всех сохранённых правил.
См. также: specification.md §8.3 Policy DSL, contracts/test-corpus/dsl/ — golden examples.
1. Scope и не-цели
Что DSL умеет:
- Boolean expressions над subject dimensions, derived metrics, ML model outputs, consent state.
- Time arithmetic (now() - duration).
- List membership (
IN,NOT IN). - Simple numeric и string comparisons.
Что DSL НЕ умеет (намеренно, для SMT-friendliness и privacy):
- Variables / assignments.
- Function definitions.
- Loops, recursion.
- I/O (нет file/network/random).
- Mutable state.
- Side effects.
- Type coercion из string в number (только numeric ↔ numeric).
- Прямой доступ к raw events (только derived dimensions и metrics).
- Hard targeting по
subject.archetypeвapplies_when(privacy invariant §9.2.6).
Каждое валидное DSL-выражение может быть транспилировано в SMT-LIB (bool + Int/Real + String + Array) для conflict/dead-rule analysis (см. §8.5).
2. Lexical structure
2.1 Whitespace и комментарии
WS := ( ' ' | '\t' | '\n' | '\r' )+
COMMENT := '#' ~'\n'* '\n'Whitespace и comments значимы только как separators, отбрасываются перед парсингом.
2.2 Tokens
| Token kind | Regex | Примеры |
|---|---|---|
IDENT | [a-z_][a-z0-9_]* | subject, tier, has_paid |
INT | -?[0-9]+ | 42, -7, 0 |
FLOAT | -?[0-9]+\.[0-9]+ | 0.75, -1.5 |
STRING | '([^'\\]|\\.)*' | 'pro', 'pt-BR' |
DURATION | [0-9]+[smhdw] | 7d, 30m, 60s, 2w |
BOOL_LITERAL | true | false | |
NULL_LITERAL | null | |
OP_* | = != < <= > >= + - * / | арифметика и сравнения |
KW_* | AND OR NOT IN NOT IN MATCHES | uppercase only |
LPAREN/RPAREN | ( ) | |
LBRACKET/RBRACKET | [ ] | array literal |
COMMA/DOT/AT | , . @ |
Strings: только single-quoted. Внутри — backslash escape: \', \\, \n, \t. Никаких "double" quotes — занято под template params в Handlebars (§7.8).
Durations: unit suffixes — s (seconds), m (minutes), h (hours), d (days), w (weeks). Нет M (months), y (years) — variable length, не SMT-friendly.
2.3 Reserved words
AND OR NOT IN MATCHES
TRUE FALSE NULL
nowReserved words НЕЛЬЗЯ использовать как identifiers. Case-sensitive: AND reserved, and — обычный identifier (но ни одна dimension не использует and как name, см. §3.2).
3. Identifiers и namespaces
3.1 Namespaces
Все идентификаторы dotted. Pulse fixes четыре namespaces:
| Namespace | Что это | Откуда resolve'ится |
|---|---|---|
subject.<dim> | Subject dimension | semantic-layer dimensions (snapshot lookup at decision time) |
subject.features.<metric>[@v<n>] | Subject-level derived metric | semantic-layer metrics (Feature Store lookup) |
subject.preferences.<key> | Subject's stored preferences | semantic-layer dimensions (subject.preferences.*) |
ml.<model>@v<n> | ML model output | ML registry inference cache (M2+) |
consent.granted(<purpose>) | Boolean: subject has granted given purpose | Consent Store |
now() | Current timestamp (UTC, decided_at) | DSL runtime |
3.2 Resolution rules
subject.<X>:XMUST match exactly one entry в activesemantic-layer/*.yamldimensions. Unknown identifier → compile error E002.subject.features.<M>:MMUST match exactly one entry в active semantic-layer metrics. Если нет@v<n>суффикса — берётся latest version, validator выдаёт warning W001 «implicit metric version» (рекомендуется explicit pin).ml.<X>@v<n>:X@v<n>MUST match registered model. Без@v<n>— compile error (модели обязаны быть pinned).consent.granted('<purpose>'):<purpose>MUST exist вconsent-purposes.yaml. Возвращаетbool.now(): возвращаетtimestamp. Никаких параметров. Resolved todecided_atдля конкретного decision, чтобы replay был детерминистичен.
3.3 Privacy whitelist (compile-time enforced)
В applies_when rules (но НЕ в cohorts!) запрещён прямой subject.archetype как hard equality / IN clause. Это compile error E004 «hard archetype targeting forbidden». См. §9.2.6 для обоснования. Шаблоны и их template_variants[].archetype_affinity — единственный путь archetype-influence.
4. Type system
4.1 Базовые типы
bool — true | false
integer — i64
float — f64
string — UTF-8, immutable
timestamp — UTC instant (RFC 3339)
duration — i64 seconds (signed; negative для "to past")
list<T> — homogeneous, M0 max 64 элемента
null — special, see §4.44.2 Type inference
Полностью inferenced; нет user-supplied type annotations. Каждое подвыражение имеет один тип; validator выдаёт E003 «type mismatch» иначе.
Идентификатор-типы определяются semantic-layer:
subject.tier→ string (enum value).subject.has_paid→ bool.subject.installed_at→ timestamp.subject.features.app_sessions_last_30d@v1→ integer.subject.features.email_open_rate@v1→ float.consent.granted('X')→ bool.now()→ timestamp.
4.3 Coercion rules
Допустимые неявные:
integer→float(для арифметики и сравнений).null→ любой type T (но валидные операции с null — толькоIS NULL/IS NOT NULL/coalesce(<expr>, <default>); в булевых contexts null трактуется как false).
Запрещённые:
string↔integer/float.string→timestamp(нетparse_timestamp('...')— лишний attack surface).bool↔ что-либо ещё.
4.4 NULL semantics
Subject dimensions могут быть NULL (см. semantic-layer nullable: true):
subject.timezone— NULL если subject не отправилapp.startup.timezonefield.subject.first_paid_at— NULL для free-tier subjects.subject.archetype— NULL если не self-reported.
Сравнения с NULL дают NULL (SQL three-valued logic). Validator выдаёт W002 «possible NULL evaluation» если выражение не покрывает NULL case явно (через IS NULL или coalesce).
В boolean expression context NULL → false (для applies_when это означает «правило не сработает»). Это safe default: rule не сработает = action не dispatch'нется.
5. Operator grammar (precedence от низкого к высокому)
expr ::= or_expr
or_expr ::= and_expr ( 'OR' and_expr )*
and_expr ::= not_expr ( 'AND' not_expr )*
not_expr ::= 'NOT' not_expr | cmp_expr
cmp_expr ::= add_expr ( cmp_op add_expr )?
| add_expr 'IN' '(' literal_list ')'
| add_expr 'NOT' 'IN' '(' literal_list ')'
| add_expr 'MATCHES' STRING # regex; M2+, для M0 — compile error
| add_expr 'IS' 'NULL'
| add_expr 'IS' 'NOT' 'NULL'
cmp_op ::= '=' | '!=' | '<' | '<=' | '>' | '>='
add_expr ::= mul_expr ( ( '+' | '-' ) mul_expr )*
mul_expr ::= unary_expr ( ( '*' | '/' ) unary_expr )*
unary_expr ::= '-' unary_expr | primary
primary ::= literal
| identifier_path
| function_call
| '(' expr ')'
literal ::= INT | FLOAT | STRING | DURATION | BOOL_LITERAL | NULL_LITERAL
literal_list ::= literal ( ',' literal )*
identifier_path ::= IDENT ( '.' IDENT )* ( '@v' INT )?
function_call ::= IDENT '(' ( expr ( ',' expr )* )? ')'5.1 Type rules для операторов
| Operator | Operand types | Result type | Notes |
|---|---|---|---|
AND, OR | bool, bool | bool | Short-circuit (но validator не assume) |
NOT | bool | bool | |
=, != | T, T где T ∈ | bool | |
<, <=, >, >= | T, T где T ∈ | bool | |
+, - | numeric или timestamp ± duration | numeric / timestamp | timestamp + timestamp — error E003 |
*, / | numeric | numeric | |
IN | T, list<T> | bool | |
NOT IN | то же | bool | |
IS NULL | T? | bool | T? = nullable T |
MATCHES | string, string (regex) | bool | M2+; для M0 — error |
5.2 Time arithmetic
now() - 7d → timestamp - duration → timestamp
subject.installed_at < now() - 7d → bool
now() - subject.installed_at → duration
(now() - subject.installed_at) > 30d → booltimestamp - timestamp → duration разрешено только в скобках, для clarity.
6. Built-in functions
| Function | Signature | Описание |
|---|---|---|
now() | () → timestamp | Decision-time timestamp (UTC). Replay-stable. |
consent.granted(purpose) | (string) → bool | True iff subject has granted given purpose. |
coalesce(expr, default) | (T?, T) → T | Replace NULL с default. |
days_since(ts) | (timestamp) → integer | Convenience: (now() - ts) / 1d clamped to non-negative. |
length(list) | (list<T>) → integer | |
contains(list, value) | (list<T>, T) → bool | Alias of value IN list. |
Список замкнут; user-defined functions запрещены.
7. Examples
7.1 Cohort definition (semantic-layer)
# installed_but_unpaid
subject.has_paid = false
AND subject.installed_at < now() - 7d7.2 Rule applies_when
# Promote upgrade banner to engaged free users.
subject.tier = 'free'
AND subject.features.app_sessions_last_30d@v1 >= 10
AND subject.installed_at < now() - 14d
AND NOT consent.granted('marketing.email') # тогда показываем баннер, а не email7.3 С NULL handling
# Subject's timezone known + опоздавшие onboarding
coalesce(subject.timezone, 'UTC') = 'Asia/Tokyo'
AND subject.onboarding_completed_at IS NULL
AND days_since(subject.installed_at) >= 27.4 ML-gated rule (M2+)
ml.churn_risk@v3 > 0.7
AND subject.tier IN ('pro', 'pro_plus_relay')
AND consent.granted('personalization')7.5 Запрещено (compile errors)
# E004 — hard archetype targeting в applies_when
subject.archetype = 'fighter' AND subject.tier = 'free'
# E005 — forbidden operator (&& вместо AND)
subject.tier = 'free' && subject.has_paid = false
# E003 — type mismatch
subject.tier > 5
subject.installed_at + 7 # int, not duration
# E002 — unknown identifier
subject.unknown_field = true
# E003 — ML model без version pin
ml.churn_risk > 0.58. Error model
8.1 Error codes
| Code | Severity | Описание |
|---|---|---|
| E001 | error | Syntax error — parser couldn't recover |
| E002 | error | Unknown identifier (dimension / metric / model / purpose) |
| E003 | error | Type mismatch |
| E004 | error | Privacy invariant violation (см. §9.2.6, §3.6) |
| E005 | error | Forbidden operator (e.g. &&, ` |
| E006 | error | Reserved word used as identifier |
| E007 | error | Function arity mismatch |
| E008 | error | Unresolved metric version (ml.X без @v<n>) |
| E009 | error | Forbidden language feature (variables, loops, etc.) |
| W001 | warning | Implicit metric version — рекомендуется @v<n> pin |
| W002 | warning | Possible NULL evaluation — рекомендуется explicit IS NULL / coalesce |
| W003 | warning | Constant expression — выражение всегда true или всегда false |
8.2 Error format в /dsl/validate response
См. contracts/openapi.yaml DslValidateResponse:
{
"valid": false,
"errors": [
{
"code": "E004",
"message": "Hard archetype targeting forbidden in applies_when (spec §9.2.6)",
"location": { "line": 3, "column": 7, "offset": 42 },
"hint": "Use template_variants[].archetype_affinity for soft personalization"
}
],
"warnings": [],
"smt_status": "unknown"
}Все error codes стабильны — изменение коду = breaking change (CI gate via test corpus).
9. SMT compilation (M2+)
Каждое валидное DSL-выражение транспилируется в SMT-LIB 2 формулу для analysis:
| DSL | SMT-LIB |
|---|---|
bool | Bool |
integer | Int |
float | Real |
string (enum) | (declare-datatypes ...) finite enum |
timestamp | Int (Unix epoch seconds) |
duration | Int (seconds) |
subject.X | uninterpreted constant per subject |
subject.features.M@vN | то же |
consent.granted('p') | uninterpreted Bool per (subject, p) |
now() | uninterpreted Int (decision_time) |
AND/OR/NOT | and/or/not |
=/</... | =/</... |
IN (...) | or of = |
Validator runs Z3 для:
- Satisfiability — может ли rule ever fire? (
unsat→ dead rule, W003). - Conflict detection — два rules с противоречивыми
applies_when+priorityordering (M3+).
Подробности — specification.md §8.5.
10. Reference grammar (PEG, для pest crate)
Лежит в contracts/test-corpus/dsl/pulse-dsl.pest (создаётся в M1 implementation).
Реализация parser: crate pulse-dsl-parser в kontinuum-pulse/crates/, добавится в M1.
11. Changelog
| Версия | Дата | Изменения |
|---|---|---|
| v0.1 | 2026-05-20 | Initial draft. Lexical structure, type system, operator precedence, error codes, examples, SMT compilation outline. Synced с spec v0.14.5. |