MemQL Specifications
What Specs Are
Specs are atomic, named boolean predicates. A spec binds exactly one shape XOR concept in its signature (epic #2281), and its body is a lambda: one parameter, then one boolean expression that reads what the signature binds through that parameter:
spec <boundName> <name> = row => <boolean expression><boundName> resolves through the file-top use import (a shape import
binds a shape, a concept import binds a concept). The binding picks how
the spec is evaluated:
- Row-specs -- bind a concept or a
@rowshape. The expression compiles into a SQLWHEREfragment and pushes down to the database for filtering. - Context-specs -- bind an
@actorshape (the only gateway to the auth envelope). The parameter is spelledactorand is the envelope:spec actorEnvelope requiresOwner = actor => actor.role == "owner". The expression evaluates in-process against the auth context; apply it toactorin a filter (requiresOwner(actor)), or call it from Go withengine.EvaluateSpec(ctx, "name").
The body reads every field through the parameter -- row.archived,
row.createdAt -- never as a bare name. A trait is the one
deliberately-unbound row predicate: trait isActiveRecord = row => row.active == true, with its fields checked against the concrete
concept where it is applied.
Authoring rules
- The body is one boolean expression after the lambda header. There is
no truthiness:
row => row.titleis refused, because a string is not a condition. - A spec or trait is applied to its receiver, like a function:
isArchivedArtifact(row),requiresOwner(actor). - Side-effect free. Specs cannot call mutation functions or logic functions.
- Named for the predicate they express, with no kind prefix
(
requiresOwner,isActiveRecord). Thespec/traitkeyword already marks the kind at the declaration. - The brace body
spec <boundName> <name> { return <expression> }is retired in edition 2026: the parser refuses it and namesmemqlmigrate --rewrite=expressions, which writes the lambda form. The@shape("name")annotation and the legacyfunc (Spec) name(ctx any) bool { ... }form are retired too.
Examples
Row-spec (SQL pushdown)
use library.concepts.{ artifact }use library.shapes.{ artifactFull } @description("Matches archived artifacts")spec artifact isArchivedArtifact = row => row.archived == true @description("Archived artifacts filed by system automation")spec artifact systemArchivedArtifact = row => row.archived == true && row.createdBy == "system:automation"Applied to the row inside a query's filter clause. The query binds its
concept in the signature (query <Concept> <name>) and pulls cross-file
constructs in via file-top use imports; predicates compose with &&,
||, ! and parentheses:
query artifact archivedArtifacts { args { folderId string @required } filter row => row.folderId == args.folderId && isArchivedArtifact(row) shape artifactFull}(Both blocks are parts of one file -- the two use lines above are its
imports, and the query resolves isArchivedArtifact and artifactFull
through them.)
(The ;-AND / ,-OR separators are retired: the parser refuses them
and names && / ||. The pre-v1 bare reference isArchivedArtifact
is what memqlmigrate --rewrite=expressions rewrites to
isArchivedArtifact(row).)
Context-spec (in-process)
use common.shapes.{ actorEnvelope } @description("Caller must hold the owner role -- the rollback gate (#1876).")spec actorEnvelope requiresClusterOwner = actor => actor.role == "owner"(The shipped spec of exactly this shape is requiresOwner, in
dsl/deployment/specs.memql. The example declares its own name because a
shipped construct resolves requiresOwner BY BARE NAME: shipped queries
apply requiresOwner(actor) in their filters, and a second declaration makes
that name ambiguous, so those filters stop lowering and the engine refuses the
load. The rule is about bare-name resolution, not about duplicate names in
general -- a spec or trait applied in a shipped filter, or a query or logic a
shipped tool's @handler names, is what breaks; a second provider, shape
or builtin of a shipped name loads.)
A ROLE COMPARISON IS THE ONE THING THIS FORM IS NOW WRONG FOR (epic
memql#5166). requiresOwner survives because owner is the cluster-owner tier
rather than a rung anybody authors around; the three specs that named admin and
developer are DELETED, because a slug comparison cannot see a role a cluster
authored for itself -- role == "admin" is false for a rank-250 role holding
every principal verb. Write @requiresRank("<slug>") for a floor on the ladder
or @requiresCapability("<verb>", "<resource>") for a grant; both are validated
at load and enforced at execution.
(The actorEnvelope @actor shape is the gateway to the auth envelope;
the spec reads its projected key -- role -- through its actor
parameter. The signature binding is verified at load: it must resolve
to an imported shape or concept, or the load refuses it with the rule id
signature_concept_unresolved that every construct signature shares,
memql#5433.)
A context-spec is applied to actor in a filter --
filter row => requiresOwner(actor) && row.status == "open" -- or, from
Go, evaluated with engine.EvaluateSpec(ctx, "name") against the
request's auth context.
CQS interaction
Compile-time CQS validation enforces:
- Query -> Mutation: not allowed
- Spec -> Mutation: not allowed
- Mutation -> Mutation: not allowed (single
insert { ... }/update { ... }block per body)
This keeps the read path side-effect-free and makes the SQL-pushdown case for row-specs always safe.
Specs vs policies
Specs are the only DSL surface for boolean predicates. The
decision-policy tier that once hosted caller-based authz /
feature-gating decisions (func (Policy) bodies, @tier /
@audited / @traces_persisted annotations, engine.EvaluatePolicy)
was retired in memql#984 -- it carried zero live constructs and the
machinery has been fully removed. The parser rejects authored
decision-policy bodies.
What remains:
- The live
policyconstruct is an empty-bodied AI provider-selection record (@primary, repeatable@fallback, and@description; see policy in the attribute matrix), consolidated indsl/policies/policies.memqland consumed by the AI Router. It is not a predicate surface. - Caller-context boolean checks (is admin, owns partition,
permission gates) are authored as context-specs in
dsl/<namespace>/specs.memqland applied asname(actor)/ evaluated withengine.EvaluateSpec. - Risk/scope decision logic lives in Go (
component/safety).