Перейти к содержимому

Селекторы и запросы

Глава 8 показала, как show, hide, group by и style выбирают срезы модели. Каждая из этих клауз принимала аргумент одного и того же вида — голое имя, атом аспекта, шаблон ребра, соединённые через and/or/not/in — не останавливаясь, чтобы объяснить, что это за аргумент такое. Это селектор: один небольшой язык запросов, дословно общий для проекций, для политик (forbid, require) и для именованных объявлений query, которые можно написать один раз и переиспользовать везде.

query PciServices: service and @@security.zone:"PCI"
policy PciIsolation {
"PCI workloads never talk to the public zone directly"
forbid PciServices > @@security.zone:"Public"
}
view PciBoard {
"PCI-scoped services, violations flagged"
show PciServices
style violating PciIsolation { color: crimson }
}

Один запрос — PciServices — питает и forbid политики, и show проекции; находки политики питают style violating проекции. Одна алгебра, три потребителя. В этом и суть главы.

На этих трёх идеях держится всё остальное:

  1. Одна алгебра, два сорта. Селектор именует либо множество узлов (модули, интерфейсы, процессы, пространства), либо множество рёбер (вызовы и инвокации do, которые выводят процессы). Какой именно сорт — решается по одному только тексту: наличие стрелки делает селектор рёберного сорта, и ничто другое этого не делает.
  2. Сигилы отмечают ось. Голое имя — это ссылка на элемент или тип; @path читает поле или встроенный геттер; @@key читает ось аспекта; $name читает ручку; * значит «всё». Что означает фрагмент, понятно без знания того, где он стоит.
  3. Селекторы и выражения встречаются только в порталах. where (…), exists (…), count (…), sum @g of (…) — порталы: конструкции в скобках, вводимые ключевым словом. Внутри них грамматика начинается заново: * — это «всё» в селекторе и умножение в выражении. Два языка никогда не смешиваются в середине терма.

Каждый селектор — либо узлового сорта (он обозначает элементы), либо рёберного сорта (он обозначает производные рёбра). Правило чисто текстовое: фрагмент, содержащий стрелку (>, >>, <>), — рёберного сорта; всё остальное — узлового.

query PciTeam: service and @@security.zone:"PCI" // узловой сорт — нет стрелки, множество элементов
query PciEgress: @@security.zone:"PCI" > * // рёберный сорт — решает стрелка

and/or требуют, чтобы обе стороны были одного сорта — and, смешивающий узловой терм с рёберным, это диагностика, а не молчаливая переинтерпретация. Приоритет, от самого слабого к самому сильному: or < and < not < голый терм (атом, стрелка, извлечение). Скобки переопределяют порядок:

view PrecedenceDemo {
show service and in Payments or database // читается как (service and in Payments) or database
hide not @@team // дополнение — всё без команды
}

hide not @@team допустим сам по себе — селектор, целиком построенный из предикатов (not, in, on, where), узлового сорта над неявным универсумом всего, что есть в области видимости.

Строительные блоки селектора узлового сорта:

АтомЗначение
*всё, что есть в области видимости
service (имя типа)экземпляры этого типа — включая подтипы
module interface surface process subprocess spaceкаждый элемент этого встроенного вида, любого типа
Payments (ссылка на элемент)этот конкретный элемент
@@keyвсё, что несёт этот ключ аспекта, с любым значением
@@key:"value" / @@key:Placeносители этого значения или этого материализованного места
$nameручка вида «элемент»
thisсвязанный субъект — только внутри портала или обязательства require
outsideэлементы, не выбранные в этой проекции — только для сторон ребра в hide/style
violating <Policy>активные находки этой политики — сорт следует за политикой

Две строки выглядят похоже, но значат разное: имя типа (service или пользовательский тип вроде pci_service) совпадает с экземплярами этого типа и его подтипов; ключевое слово вида (module, process, …) совпадает с каждым элементом этого структурного вида, независимо от типа. service и process — равноправные строки таблицы, но отвечают на разные вопросы: «какой тип» против «какая форма».

export type service pci_service {
"A service in PCI scope."
}
query PciByType: pci_service // совпадает с экземплярами pci_service — и любыми ЕГО подтипами тоже

@@ в селекторе всегда означает ось аспекта — никогда не aspect, которое остаётся только для объявлений. Атом аспекта со значением слитный: между ключом, : и значением нет пробела:

view ZoneScope {
"One security zone, picked at open time"
knob zone from @@security.zone
show @@security.zone:$zone
}

violating <Policy> выбирает только активные находки политики — исключённые находки живут в отчёте по исключениям, а не на доске:

policy PciIsolation {
forbid @@security.zone:"PCI" > @@security.zone:"Public"
}
query PciFindings: violating PciIsolation // здесь рёберный сорт — PciIsolation это forbid над стрелкой

Термы соединяются только явными and/or; not ставится перед любым термом. Наряду с атомами, три предиката фильтруют конъюнкцию — у них нет собственного сорта, поэтому они принимают тот сорт, что и остальная часть конъюнкции:

ПредикатЗначение (для узлового сорта)
in <Module | Space>структурный потомок или участник этого пространства
in <Process> / in thisучаствует в этом процессе
on <plane>присутствует на этой плоскости
where ( <expr> )предикат-выражение — см. Выражения ниже
query PaymentsInternals: in Payments // всё, что структурно находится внутри Payments
query DeploymentTargets: service and on deployment // сервисы, присутствующие на плоскости deployment
query BigFanIn: where (@connections > 20) // предикат-выражение, сам по себе

in this — единственный предикат, чей смысл зависит от того, к чему он привязан: привязанный к процессу, он значит «участвует в этом процессе»; привязанный к структурному субъекту (модулю, пространству) — «является потомком этого». Он разбирается только там, где субъект действительно связан — внутри портала where/exists/агрегата или как субъектная сторона обязательства require.

У стрелки всегда записаны обе стороны — никаких открытых сторон, никакого соположения. Сторона — это атом, this, $knob, * или селектор узлов в скобках:

query DirectToPayments: service > Payments // прямые рёбра service → Payments
query UpstreamOfPayments: * >> Payments // весь восходящий конус, как рёбра
query NearPayments: * <> Payments within 2 // рёбра в пределах 2 неориентированных переходов
  • X > Y — базовые рёбра от X к Y.
  • X >> Y — рёбра на любом ориентированном пути длины ≥ 1 от узла-X к узлу-Y. exists (this >> Payments) читается как «в конце концов достигает Payments».
  • X <> Y — рёбра с одним концом на каждой стороне, в любом направлении. X <> Y within N ограничивает длину пути; голое X <> Y — это within 1.

within N (число или $knob) допустим только при >>/<> — если написать его после голого >, это адресная диагностика, указывающая на >>.

Цепочки — это объединение переходов, а не ограничение пути. A > B > C выводится точно так же, как собственная цепочка вызовов процесса, — два независимых ребра, объединённых, а не «путь через B»:

query CheckoutHops: Customer > Gateway > Orders > Payments
// ≡ (Customer > Gateway) or (Gateway > Orders) or (Orders > Payments)

Поскольку это объединение, цепочка внутри обязательства require — это ловушка: только первый переход ограничивает this, поэтому require service: this > B > C проходит впустую в тот момент, когда где угодно существует хоть одно ребро B > C. Тянитесь к this >> C (достижимость), когда имеете в виду настоящее многопереходное обязательство.

in <Process>/in this, on <plane> и where (<expr>) тоже сочетаются с конъюнкцией рёберного сорта — where там ограничен одним рёберным встроенным геттером, @kind (call или do):

query InvocationEdges: (* > *) and where (@kind = "do") // только рёбра-инвокации (do)

in this, привязанный к процессу, втягивает собственные производные рёбра этого процесса — включая рёбра через любой подпроцесс или процесс, который он вызывает через do. Это сквозное включение важно: без него аудиторское обязательство ложно отмечало бы каждый процесс, который отчитывается через общий хелпер do EmitOpsEvent, вместо прямого вызова журнала аудита.

policy ProcessesAudited {
require process: (* > AuditLog) and in this
}

Четыре извлечения превращают селектор рёберного сорта обратно в множество узлов — единственное преобразование сорта в языке:

query CallersOfPayments: sources of (* > Payments) // исходные конечные точки
query CalleesOfPayments: targets of (Payments > *) // конечные точки назначения
query PaymentsNeighborhood: Payments or nodes of (* <> Payments within 2)
query PaymentsProcessOwners: owners of (process and in Payments) // кто владеет шагами этих процессов

sources of/targets of нуждаются в ориентированном шаблоне (>/>>); для <> поддерживается только nodes of. И они намеренно точны — количество входящих рёбер и количество входящих вызывающих — это разные вопросы с разной записью: count (* > Payments) против count (sources of (* > Payments)) (два ребра от одного и того же вызывающего во втором случае считаются один раз, в первом — дважды).

nodes of/sources of/targets of никогда не изобретают якорь: nodes of (* <> X) пусто, если у X вообще нет рёбер. Пишите якорь явно, как это делает PaymentsNeighborhood выше (Payments or nodes of (…)).

owners of — единственное извлечение, которое заглядывает внутрь процесса: оно возвращает всех, кто владеет каким-либо владеемым шагом — вызовом, заметкой, await, головой управляющей конструкции — для процессов/подпроцессов в своём аргументе. Шаг, владеемый TODO, добавляет свою синтезированную заглушку, которая не несёт никаких аспектов, — поэтому приведённое ниже правило про зоны отказывает по умолчанию (fail closed) на неразмеченной работе, а не молчаливо её пропускает:

policy PciControl {
"No Public-zone decision-maker inside a PCI process"
forbid (process and @@security.zone:"PCI"
and where (exists ((owners of (this)) and not @@security.zone:"PCI")))
}

where (<expr>), exists (<selector>), count (<selector>) и <agg> @g of (<selector>) — это порталы, где живёт язык выражений: одна и та же грамматика, общая для предикатов where, столбцов таблиц и вычисляемых значений style (Глава 8).

Геттеры снабжены сигилами, как и везде: @version/@repo.url читают поля (Глава 9), @@team/@@security.zone читают аспекты. Голый идентификатор в выражении — это литерал, а не ссылка — where (@status = active) сравнивает со значением-идентификатором active, а не с элементом по имени active.

Небольшой набор встроенных геттеров — зарезервированных имён геттеров, годных без каких-либо объявлений:

Встроенный геттерСубъектЗначение
@nameлюбой узелимя элемента
@typeлюбой узелтип элемента
@connectionsлюбой узелчисло касающихся его рёбер
@todosлюбой узелчисло открытых TODO
@kindреброcall | do
@estimatedLatencyпроцесс/подпроцесснаихудшая системная задержка
@distпроцесс/подпроцессистинно, если хоть одно владеемое действие разрешается в эмерджентное управление
@reversible / @compensatedпроцесс/подпроцесссодержит сагу / эта сага полностью покрыта компенсацией
policy LatencyBudget {
severity: warning
forbid process and where (@estimatedLatency > 500ms)
}

Литерал длительности вроде 500ms сравнивается напрямую с @estimatedLatency — сравнение приводит обе стороны к секундам, поэтому геттер поля в форме latency работает точно так же.

Операторы: сравнения = != < <= > >=, булевы and or not, арифметика + - * /, приоритет от самого слабого к самому сильному: or < and < not < сравнение < + - < * /. Скобки группируют как обычно:

policy CapacityGuards {
severity: advisory
forbid @@domain and where (sum @connections of (service and in this) > 300)
}

Агрегаты: count (<sel>) над любым сортом, count distinct @g of (<sel>) и sum|min|max @g of (<sel>) (им нужен селектор узлового сорта, поскольку они читают геттер для каждого элемента):

view ProcessAudit {
show process and in Checkout
table {
column @name
column "Teams": count distinct @@team of (service and in this and not Gateway)
}
}
policy PaymentsCostBudget {
severity: warning
forbid Payments and where (max @cost of (* > Payments) > 500)
}

exists вычисляет селектор с this, привязанным к текущему субъекту, и истинен, если результат непуст, — одно правило и для членства (exists (this and PiiServices)), и для достижимости (exists (this >> PiiServices)). Именно это заставляет работать правило про owners of выше (Извлечение): exists ((owners of (this)) and not @@security.zone:"PCI") читается как «какой-то из владельцев шагов этого процесса НЕ в зоне PCI».

Функции — это явные приведения, никогда не неявные: colorize(v) (категориальное значение → легенда с образцом на каждое значение), heat(v [, min, max]) (числовое значение → градиент), css(v) (значение уже цвет, используется как есть). Они существуют потому, что значение никогда не становится цветом рендера молча — вы видели, как они управляют style в Главе 8; приведение — это одна и та же функция, независимо от того, написана она внутри блока style или в любом другом слоте выражения.

query Name: <selector> объявляет переиспользуемый селектор — на верхнем уровне файла или внутри тела модуля. Запрос несёт свой сорт: запрос узлового сорта используйте там, где ожидается селектор узлового сорта, рёберного — там, где ожидается селектор рёберного сорта. Использование там, где сорт не подходит, — это адресная диагностика, а не молчаливая переинтерпретация.

query PiiServices: service and @@data:"pii"

export query публикует запрос через границы пакета так же, как экспортированный подпроцесс пересекает пространства, — экспортировать можно только запросы верхнего уровня файла:

export query PciServices: @@security.zone:"PCI"

Вступительный пример уже показал выгоду: один запрос питает show, forbid политики и получившийся style violating. Вот вся дуга ещё раз, расписанная от начала до конца — объявить один раз, переиспользовать в выборке проекции, в правиле политики и в стилизации соответствия этой же проекции:

query PiiServices: service and @@data:"pii"
policy PiiAudited {
"Every PII-touching service is audited somewhere downstream"
severity: warning
require PiiServices: this >> Audit
}
view PiiExposure {
"Everything that can reach PII data, PII services flagged"
show PiiServices or nodes of (* >> PiiServices within 3)
style violating PiiAudited { color: crimson }
style PiiServices { widget { icon: shield } }
}

PiiServices никогда не приходится выводить заново — show проекции, субъект require политики и (транзитивно, через политику) style violating этой же проекции читают одно и то же объявление. Переименуйте ключ аспекта один раз, в одном месте, — и каждый потребитель останется корректным.

Глава 32: Политики и управление →forbid, require, исключения except и гейты изменений when, построенные на ядре селекторов из этой главы.