Skip to content

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 kindRegexПримеры
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_LITERALtrue | false
NULL_LITERALnull
OP_*= != < <= > >= + - * /арифметика и сравнения
KW_*AND OR NOT IN NOT IN MATCHESuppercase 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
now

Reserved 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 dimensionsemantic-layer dimensions (snapshot lookup at decision time)
subject.features.<metric>[@v<n>]Subject-level derived metricsemantic-layer metrics (Feature Store lookup)
subject.preferences.<key>Subject's stored preferencessemantic-layer dimensions (subject.preferences.*)
ml.<model>@v<n>ML model outputML registry inference cache (M2+)
consent.granted(<purpose>)Boolean: subject has granted given purposeConsent Store
now()Current timestamp (UTC, decided_at)DSL runtime

3.2 Resolution rules

  • subject.<X>: X MUST match exactly one entry в active semantic-layer/*.yaml dimensions. Unknown identifier → compile error E002.
  • subject.features.<M>: M MUST 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 to decided_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.4

4.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

Допустимые неявные:

  • integerfloat (для арифметики и сравнений).
  • null → любой type T (но валидные операции с null — только IS NULL / IS NOT NULL / coalesce(<expr>, <default>); в булевых contexts null трактуется как false).

Запрещённые:

  • stringinteger / float.
  • stringtimestamp (нет parse_timestamp('...') — лишний attack surface).
  • bool ↔ что-либо ещё.

4.4 NULL semantics

Subject dimensions могут быть NULL (см. semantic-layer nullable: true):

  • subject.timezone — NULL если subject не отправил app.startup.timezone field.
  • 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 для операторов

OperatorOperand typesResult typeNotes
AND, ORbool, boolboolShort-circuit (но validator не assume)
NOTboolbool
=, !=T, T где T ∈bool
<, <=, >, >=T, T где T ∈bool
+, -numeric или timestamp ± durationnumeric / timestamptimestamp + timestamp — error E003
*, /numericnumeric
INT, list<T>bool
NOT INто жеbool
IS NULLT?boolT? = nullable T
MATCHESstring, string (regex)boolM2+; для 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 → bool

timestamp - timestamp → duration разрешено только в скобках, для clarity.

6. Built-in functions

FunctionSignatureОписание
now()() → timestampDecision-time timestamp (UTC). Replay-stable.
consent.granted(purpose)(string) → boolTrue iff subject has granted given purpose.
coalesce(expr, default)(T?, T) → TReplace NULL с default.
days_since(ts)(timestamp) → integerConvenience: (now() - ts) / 1d clamped to non-negative.
length(list)(list<T>) → integer
contains(list, value)(list<T>, T) → boolAlias 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() - 7d

7.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')        # тогда показываем баннер, а не email

7.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) >= 2

7.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.5

8. Error model

8.1 Error codes

CodeSeverityОписание
E001errorSyntax error — parser couldn't recover
E002errorUnknown identifier (dimension / metric / model / purpose)
E003errorType mismatch
E004errorPrivacy invariant violation (см. §9.2.6, §3.6)
E005errorForbidden operator (e.g. &&, `
E006errorReserved word used as identifier
E007errorFunction arity mismatch
E008errorUnresolved metric version (ml.X без @v<n>)
E009errorForbidden language feature (variables, loops, etc.)
W001warningImplicit metric version — рекомендуется @v<n> pin
W002warningPossible NULL evaluation — рекомендуется explicit IS NULL / coalesce
W003warningConstant expression — выражение всегда true или всегда false

8.2 Error format в /dsl/validate response

См. contracts/openapi.yaml DslValidateResponse:

json
{
  "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:

DSLSMT-LIB
boolBool
integerInt
floatReal
string (enum)(declare-datatypes ...) finite enum
timestampInt (Unix epoch seconds)
durationInt (seconds)
subject.Xuninterpreted constant per subject
subject.features.M@vNто же
consent.granted('p')uninterpreted Bool per (subject, p)
now()uninterpreted Int (decision_time)
AND/OR/NOTand/or/not
=/</...=/</...
IN (...)or of =

Validator runs Z3 для:

  • Satisfiability — может ли rule ever fire? (unsat → dead rule, W003).
  • Conflict detection — два rules с противоречивыми applies_when + priority ordering (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.12026-05-20Initial draft. Lexical structure, type system, operator precedence, error codes, examples, SMT compilation outline. Synced с spec v0.14.5.