Selectors & Queries
Chapter 8 showed show, hide, group by, and style picking slices of the model. Every one of those clauses took the same kind of argument — a bare name, an aspect atom, an edge pattern, joined with and/or/not/in — without stopping to explain what that argument is. It’s a selector: one small query language, shared verbatim by views, by policies (forbid, require), and by named query declarations you can write once and reuse everywhere.
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 }}One query — PciServices — feeds a policy’s forbid and a view’s show; the policy’s findings feed the view’s style violating. Same algebra, three consumers. That’s the point of this chapter.
Three ideas carry the whole thing:
- One algebra, two sorts. A selector names either a set of nodes (modules, interfaces, processes, spaces) or a set of edges (the calls and
doinvocations processes derive). Which one is decidable from the text alone — an arrow makes it edge-sort, nothing else does. - Sigils mark the axis. A bare name is an element or type reference;
@pathreads a field or a builtin;@@keyreads the aspect axis;$namereads a knob;*is “everything.” You can tell what a fragment means without knowing where it sits. - Selectors and expressions meet only at portals.
where (…),exists (…),count (…),sum @g of (…)— parenthesized keyword portals. Inside, the grammar starts fresh:*is “everything” in a selector, multiplication in an expression. The two languages never blend mid-term.
One algebra, two sorts
Section titled “One algebra, two sorts”Every selector is either node-sort (it denotes elements) or edge-sort (it denotes derived edges). The rule is purely textual: a fragment containing an arrow (>, >>, <>) is edge-sort; everything else is node-sort.
query PciTeam: service and @@security.zone:"PCI" // node-sort — no arrow, a set of elementsquery PciEgress: @@security.zone:"PCI" > * // edge-sort — the arrow decides itand/or require both sides to share a sort — an and mixing a node term with an edge term is a diagnostic, not a silent reinterpretation. Precedence, loosest to tightest: or < and < not < a bare term (an atom, an arrow, an extraction). Parens override:
view PrecedenceDemo { show service and in Payments or database // reads as (service and in Payments) or database hide not @@team // complement — everything without a team}hide not @@team is legal on its own — a selector built entirely of predicates (not, in, on, where) is node-sort over the implicit universe of everything in scope.
Node atoms
Section titled “Node atoms”The building blocks of a node-sort selector:
| Atom | Meaning |
|---|---|
* | everything in scope |
service (a type name) | instances of that type — subtypes included |
module interface surface process subprocess space | every element of that built-in kind, across all types |
Payments (an element reference) | that one element |
@@key | anything carrying that aspect key, any value |
@@key:"value" / @@key:Place | carriers with that value or that materialized place |
$name | an element-kind knob |
this | the bound subject — only inside a portal or a require obligation |
outside | elements not selected in this view — hide/style edge sides only |
violating <Policy> | that policy’s active findings — sort follows the policy |
Two rows look similar and mean different things: a type name (service, or a user type like pci_service) matches instances of that type and its subtypes; a kind keyword (module, process, …) matches every element of that structural kind regardless of type. service and process are peers in the table but answer different questions — “which type” versus “which shape.”
export type service pci_service { "A service in PCI scope."}
query PciByType: pci_service // matches pci_service instances — and any of ITS subtypes too@@ is always the aspect axis in a selector — never aspect, which stays declaration-only. A valued aspect atom is glued: no space between the key, the :, and the value:
view ZoneScope { "One security zone, picked at open time" knob zone from @@security.zone show @@security.zone:$zone}violating <Policy> selects only a policy’s active findings — waived ones live on the waiver report, not the board:
policy PciIsolation { forbid @@security.zone:"PCI" > @@security.zone:"Public"}
query PciFindings: violating PciIsolation // edge-sort here — PciIsolation is a forbid over an arrowCombinators and predicates
Section titled “Combinators and predicates”Terms combine only with explicit and/or; not prefixes any term. Alongside atoms, three predicates filter a conjunction — they carry no sort of their own, so they adapt to whatever sort the rest of the conjunction has:
| Predicate | Meaning (node-sort) |
|---|---|
in <Module | Space> | structural descendant, or member of that space |
in <Process> / in this | participates in the process |
on <plane> | present on that plane |
where ( <expr> ) | an expression predicate — see Expressions below |
query PaymentsInternals: in Payments // everything structurally under Paymentsquery DeploymentTargets: service and on deployment // services present on the deployment planequery BigFanIn: where (@connections > 20) // an expression predicate, standing alonein this is the one predicate whose meaning depends on what it’s attached to: bound to a process, it means “participates in this process”; bound to a structural subject (a module, a space), it means “is a descendant of this.” It only parses where a subject is actually bound — inside a where/exists/aggregate portal, or as the subject side of a require obligation.
Edge patterns
Section titled “Edge patterns”An arrow always has both sides written — no open sides, no juxtaposition. A side is an atom, this, $knob, *, or a parenthesized node selector:
query DirectToPayments: service > Payments // direct edges service → Paymentsquery UpstreamOfPayments: * >> Payments // the whole upstream cone, as edgesquery NearPayments: * <> Payments within 2 // edges within 2 undirected hopsX > Y— base edges from an X to a Y.X >> Y— edges on any directed path of length ≥ 1 from an X-node to a Y-node.exists (this >> Payments)reads “eventually reaches Payments.”X <> Y— edges with one endpoint on each side, either direction.X <> Y within Nbounds the path length; plainX <> Yiswithin 1.
within N (a number or $knob) is legal only on >>/<> — writing it after a plain > is a targeted diagnostic pointing you at >>.
Chains are hop union, not a path constraint. A > B > C derives the same way a process’s own call chain does — two independent edges, unioned, not “a path through B”:
query CheckoutHops: Customer > Gateway > Orders > Payments// ≡ (Customer > Gateway) or (Gateway > Orders) or (Orders > Payments)Because it’s a union, a chain inside a require obligation is a trap: only the first hop constrains this, so require service: this > B > C passes vacuously the moment any B > C edge exists anywhere. Reach for this >> C (reachability) when you mean a real multi-hop obligation.
Edge predicates
Section titled “Edge predicates”in <Process>/in this, on <plane>, and where (<expr>) also compose onto an edge-sort conjunction — where there is limited to the one edge builtin, @kind (call or do):
query InvocationEdges: (* > *) and where (@kind = "do") // just the do/invocation edgesin this, bound to a process, pulls in that process’s own derived edges — including through any subprocess or process it invokes via do. That splice-inclusion matters: without it, an audit obligation would falsely flag every process that reports through a shared do EmitOpsEvent helper instead of calling the audit log directly.
policy ProcessesAudited { require process: (* > AuditLog) and in this}Extraction — into node sets
Section titled “Extraction — into node sets”Four extractions turn an edge-sort selector back into a node set — the only sort conversion in the language:
query CallersOfPayments: sources of (* > Payments) // origin endpointsquery CalleesOfPayments: targets of (Payments > *) // destination endpointsquery PaymentsNeighborhood: Payments or nodes of (* <> Payments within 2)query PaymentsProcessOwners: owners of (process and in Payments) // who owns those processes' stepssources of/targets of need a directed pattern (>/>>); <> only supports nodes of. And they’re precise on purpose — inbound edge count and inbound caller count are different questions with different spellings: count (* > Payments) versus count (sources of (* > Payments)) (two edges from the same caller count once in the second, twice in the first).
nodes of/sources of/targets of never invent an anchor: nodes of (* <> X) is empty if X has no edges at all. Write the anchor explicitly, as PaymentsNeighborhood does above (Payments or nodes of (…)).
owners of is the one that reaches inside a process: it returns whoever owns any owned step — a call, a note, an await, a control-construct head — for the processes/subprocesses in its argument. A TODO-owned step contributes its synthesized stub, which carries no aspects — so a zone-shaped rule below fails closed on unowned work rather than silently passing it:
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")))}Expressions
Section titled “Expressions”where (<expr>), exists (<selector>), count (<selector>), and <agg> @g of (<selector>) are the portals where the expression language lives — the one grammar shared by where predicates, table columns, and style computed values (Chapter 8).
Getters are sigiled, same as everywhere: @version/@repo.url read fields (Chapter 9), @@team/@@security.zone read aspects. A bare identifier in an expression is a literal, not a reference — where (@status = active) compares against the identifier active, not an element named active.
A short roster of builtins — reserved getter names, valid without declaring anything:
| Builtin | Subject | Meaning |
|---|---|---|
@name | any node | the element’s name |
@type | any node | the element’s type |
@connections | any node | count of edges touching it |
@todos | any node | open TODO count |
@kind | edge | call | do |
@estimatedLatency | process/subprocess | worst-case system latency |
@dist | process/subprocess | true if any owned action resolves to emergent control |
@reversible / @compensated | process/subprocess | has saga content / that content is fully covered |
policy LatencyBudget { severity: warning forbid process and where (@estimatedLatency > 500ms)}A duration literal like 500ms compares directly against @estimatedLatency — the comparison coerces to seconds on either side, so a latency-shaped field getter works the same way.
Operators: comparisons = != < <= > >=, booleans and or not, arithmetic + - * /, precedence loosest to tightest or < and < not < comparison < + - < * /. Parens group as usual:
policy CapacityGuards { severity: advisory forbid @@domain and where (sum @connections of (service and in this) > 300)}Aggregates: count (<sel>) over either sort, count distinct @g of (<sel>), and sum|min|max @g of (<sel>) (these need a node-sort selector, since they read a getter per element):
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 evaluates a selector with this bound to the current subject, true iff the result is non-empty — one rule for membership (exists (this and PiiServices)) and reachability (exists (this >> PiiServices)) alike. It’s also what makes the owners of governance rule above (Extraction) work: exists ((owners of (this)) and not @@security.zone:"PCI") reads “some owner of this process’s steps is NOT PCI-zoned.”
Functions are explicit casts, never implicit: colorize(v) (categorical → a swatch-per-value legend), heat(v [, min, max]) (numeric → a gradient), css(v) (the value is already a color, used verbatim). They exist because a value never silently becomes a render color — you saw them driving style in Chapter 8; the cast is the same function whether it’s typed inside a style block or any other expression slot.
Named queries
Section titled “Named queries”query Name: <selector> declares a reusable selector — at file top level or inside a module body. A query carries its sort: use a node-sort query wherever a node-sort selector is expected, an edge-sort one wherever an edge-sort selector is expected. Using it where the sort doesn’t fit is a targeted diagnostic, not a silent reinterpretation.
query PiiServices: service and @@data:"pii"export query publishes a query across package boundaries the same way an exported subprocess crosses spaces — only file-root queries are exportable:
export query PciServices: @@security.zone:"PCI"The opening example already showed the payoff: one query feeding a show, a policy’s forbid, and the resulting style violating. Here’s the full arc again, spelled out end to end — declare once, reuse in a view’s selection, in a policy’s rule, and in that view’s own compliance styling:
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 never has to be re-derived — the view’s show, the policy’s require subject, and (transitively, through the policy) the view’s style violating all read the same declaration. Rename the aspect key once, in one place, and every consumer stays correct.
What’s next
Section titled “What’s next”Chapter 32: Policies & Governance → — forbid, require, except waivers, and when change gates built on the selector core this chapter covered.