Skip to contentScreenplay.Company

Queries and computation

1. Evaluation contract

SSL-Q 0.1 is the optional normative query capability. It operates on an immutable, access-filtered snapshot and returns values with provenance and completeness. Every language document remains queryable through its generic records and fields even when no definition is installed. A schema is useful documentation, not a prerequisite for lookup.

A query is data. Evaluation has no network, filesystem, clock, randomness, model inference, mutation or tool invocation. A host MAY expose SQL, GraphQL or a human query shorthand; it MUST lower that request to this contract or identify the different semantics. User-authored definitions cannot install executable functions.

{
  "language": "ssl-q/0.1",
  "snapshot": "demo-1",
  "from": {"kind": "film.scene", "as": "scene", "history": false},
  "steps": [
    {"op": "where", "test": {"call": "core.eq@1", "args": [
      {"path": ["scene", "fields", "state"]}, {"lit": "draft"}
    ]}},
    {"op": "select", "columns": {"id": {"path": ["scene", "id"]},
                                  "title": {"path": ["scene", "fields", "title"]}}}
  ]
}

Top-level members are exactly language, snapshot, from, steps, optional failureMode, namespaces, parameterNames and parameters. Unknown members reject the query. namespaces inherits snapshot bindings and may add aliases/new function namespaces without rebinding existing prefixes. Core built-in function prefixes have fixed identities: core=urn:ssl:0.1:core, text=urn:ssl:0.1:function:text, list=urn:ssl:0.1:function:list, number=urn:ssl:0.1:function:number, time=urn:ssl:0.1:function:time. They are prebound for every query. A conflicting snapshot prefix is not inherited into that reserved function-prefix slot; kind queries can declare a fresh alias to the snapshot URI. An explicit query attempt to rebind a fixed function prefix is rejected. Kind and function resolution uses expanded URIs.

parameterNames is an optional array of distinct strings; parameters is an optional object of Values. Parameters are available under reserved binding params, which cannot be rebound by any stage or source. Missing parameters yield missing cells. Undeclared supplied parameters are reported and ignored. With no declaration, no parameters are permitted.

from.kind is optional; absence means every visible selected record. Kind matching compares expanded namespace URI + local term, without inheritance unless explicitly requested. as is required and names a row binding. history defaults false; true enumerates only visible supplied historical revisions. Tombstone heads are included by default and can be filtered by kind. Root order is UTF-8 bytewise ID then ascending revision. There is no implicit current-time lookup.

A read endpoint receives owner/work/session permissions outside the query. Access filtering MUST happen before selection, dereference, traversal, join and aggregation. Unavailable private targets produce the same external form as other unavailable targets. Cached results include the access scope in their key and cannot be reused across incompatible readers.

2. Expressions and result cells

Expression grammar, each form having exactly its shown discriminator and members:

{"lit": Value}
{"path": [bindingName, fieldName|arrayIndex, ...]}
{"get": Expr, "path": [fieldName|arrayIndex, ...]}
{"call": "namespace.function@positiveVersion", "args": Expr[]}
{"if": Expr, "then": Expr, "else": Expr}

A literal $ref is data until dereferenced. In lit, path and get alike, a recognised stored $unknown lifts to an unknown Cell. Raw can inspect the original stored literal/path/get value without that lifting; it cannot recover data from unavailable access. Rows bind names to Cells; source records initially bind known Cells containing their generic JSON record. Paths automatically unwrap a known Cell at the binding root; a non-known root propagates its state. Stage output cells are not traversed as authored objects (a property named state is only a real stored field). Paths use explicit array indices, never dot-split strings, wildcards or executable expressions. Missing path components return missing; null is a known value. Index must be a nonnegative safe integer. Access past a scalar returns missing with a type diagnostic. A field explicitly holding $unknown evaluates to unknown. $literal remains opaque until core.unwrap@1 is called. An unknown function returns unsupported for that cell; other rows remain available.

An evaluation cell is one of:

{state:"known", value:Value}
{state:"missing"}
{state:"unknown", reason?:string}
{state:"unavailable"}
{state:"unsupported", capability:string}
{state:"error", code:string}

These are evaluator outputs, never automatically rewritten into authored fields. Unknown source tags become unsupported only on interpretation, while core.raw@1 can expose their original inert JSON. if evaluates only the selected branch; a non-known condition returns unknown, and a known nonboolean returns type-error. All other function arguments are evaluated eagerly. No automatic string-to-number, unit or boolean coercion occurs.

3. Pipeline operators

A pipeline is an ordered array. It operates on a bag of rows; duplicate rows survive unless explicitly removed. Each stage preserves input order unless stated otherwise. All references emitted by a resolver are exact. Unknown members of a query operator are rejected as unsupported-query so a typo cannot silently alter computation; the containing story remains valid.

Operator shapeDefined behaviour
{"op":"where","test":Expr}Keep only known true. Count excluded false and unresolved separately; unresolved exclusions make result incomplete.
{"op":"let","as":name,"value":Expr}Add a binding. Duplicate binding names are errors.
{"op":"expand","value":Expr,"as":name,"outer":bool}One row per array element in stored order. Default outer=false. Empty known array yields zero rows; outer=true yields one missing binding. Non-known source also yields missing for outer, with incomplete result. Known non-array is a type error.
{"op":"resolve","ref":Expr,"as":name,"outer":bool}Resolve a reference through the snapshot. Default outer=true keeps non-known targets as unavailable bindings. outer=false drops them but marks incomplete. A tombstone resolves as a tombstone, not its predecessor.
{"op":"join","source":From,"on":Expr,"mode":"inner"|"left"}Stable nested-loop semantics over filtered source; true matches only. Left emits missing right binding when no true match. Unknown comparisons mark incomplete. Binding names must not overlap.
{"op":"select","columns":{name:Expr}}Replace row bindings with named result cells. Expressions in the same select see only the previous row.
{"op":"distinct","keys":Expr[]}Keep first row for each tuple of canonical known values. Distinct unresolved tuples are never collapsed; flag incomplete.
{"op":"sort","keys":[{"value":Expr,"direction":"asc"|"desc"}]}Stable comparison using §5. Defaults direction=asc. Original position breaks ties.
{"op":"limit","count":integer,"offset":integer}Count and offset nonnegative, offset default 0. Pagination/truncation is explicitly reported, never completeness.
{"op":"group","keys":{name:Expr},"aggregates":{name:Aggregate}}Group known tuples by canonical equality, ordered by first appearance. Every row with a non-known key gets its own unresolved group, never conflated with another row or known null. Output bindings are precisely key names plus aggregate names; duplicate names are invalid. No keys yields one group including empty input.
{"op":"walk","start":Expr,"via":[fieldPath,...],"as":name,"maxDepth":integer,"direction":"out"}§6 bounded traversal; direction out is the only 0.1 direction. Reverse edges use a join or an indexed equivalent with identical results.

From is {kind?:Term, as:name, history?:boolean}. Each fieldPath is an array of strings/indices relative to a record. Aggregate grammar is {fn:"count"|"collect"|"sum"|"min"|"max", value?:Expr, distinct?:boolean}. count without a value counts rows; with a value it counts known cells including null and flags incomplete if any input cell was non-known. Other aggregates require a value. collect returns a known array of {state,...} cell descriptions in order, preserving non-known entries as data, and marks incomplete when any entry was non-known. sum, min, max require compatible known numeric values; empty sum is integer 0, empty min/max is missing. Unknown numeric inputs make the aggregate unknown, with an optional separately labelled known-subtotal diagnostic. It MUST NOT present the subtotal as a complete total. Distinct defaults false and uses canonical equality. Mixed units or numeric types are errors unless explicitly converted.

4. Core functions

Names and versions below define exact required behaviour for SSL-Q 0.1. An implementation can advertise partial function support; unsupported requests return the capability name. Registry code is trusted application code or an interpreter for the bounded expression grammar, never downloaded code. Functions are pure and resource-bounded.

FunctionArguments and result
core.eq@1, core.ne@1Two known values; structural equality, object order ignored, array order retained. Numeric tags compare canonical exact values with identical units. Cross-type values are unequal. Non-known yields unknown.
core.lt@1, core.lte@1, core.gt@1, core.gte@1Two compatible numeric values or two strings; exact numeric / UTF-8 bytewise string order. Null and incompatible types are errors.
core.and@1, core.or@1, core.not@1Boolean three-valued logic. False AND unknown=false; true OR unknown=true; otherwise unknown propagates. Nonboolean known values are errors. and/or take exactly two args.
core.isMissing@1, core.isKnown@1, core.isNull@1One cell; inspect state without coercion. isNull true only for known null.
core.coalesce@1At least one argument; first known non-null cell, else missing. Explicit use is recorded in provenance because it discards uncertainty.
core.raw@1One literal/path/get-derived cell; expose stored Value literally including unknown tags, where readable; unavailable remains unavailable.
core.unwrap@1One known $literal; return its JSON as data. Does not execute any content.
text.contains@1Two known strings; exact case-sensitive scalar substring search.
text.length@1One string; Unicode scalar count.
text.concat@1Zero or more strings; exact concatenation.
text.startsWith@1, text.endsWith@1Two known strings; true when the first begins (ends) with the second, exact and case-sensitive by Unicode scalar. The empty string matches.
text.trim@1One string; removes ECMAScript WhiteSpace and LineTerminator code points from both ends only.
list.length@1, list.at@1Array length; array plus nonnegative index, missing if out of range.
number.add@1, number.sub@1, number.mul@1, number.div@1Two same-type exact numbers. Add/sub need equal units. Mul/div permitted only when at most the numerator/one operand has a unit; derived unit algebra requires a separate advertised function. Division by zero is an error. Integer division returns a dimensionless rational (or numerator's unit); decimal division returns rational; rational arithmetic reduces. Integer add/sub/mul return integers; decimal add/sub/mul return decimals; rational operations return rationals. Overflow of a safe integer returns error, never rounded output.
number.rational@1Integer numerator, positive integer denominator and optional unit string; reduce canonically.
time.framesToSeconds@1Integer frame count plus positive rational frames/second; returns rational seconds.
time.secondsToFrames@1Rational seconds, positive rational frames/second, mode "exact"|"floor"|"ceil"|"nearest-even"; exact rejects nonintegral result.

For number.mul, a unit on either operand is retained; for number.div, a unit on the denominator is unsupported, except two equal units cancel to dimensionless. Zero arguments for concat yields empty string. Unknowns generally propagate; inspect/coalesce/raw functions are explicit exceptions. Rational intermediate numerators may exceed safe integers internally, but an unrepresentable final tagged rational returns overflow. Decimal arithmetic uses exact arbitrary precision up to advertised limits. text.startsWith@1, text.endsWith@1 and text.trim@1 are optional versioned capabilities a host advertises in its function list; a host without them returns unsupported for that cell. Dates/currencies/unit conversions, regular expressions, optimisation solvers and language-model analyses are optional versioned capabilities with their own declared semantics. No implicit exchange rates, locale, timezone database or fuzzy matching.

5. Ordering, errors and completeness

For sort only, ordering is: known values, missing, unknown, unavailable, unsupported, error. Within known values: null, boolean (false before true), integer, decimal, rational, string, ref, array, object. A unitless value sorts before a unit-bearing value; different units sort bytewise by unit then exact magnitude; this is ordering, not physical comparison. Arrays compare lexicographically, objects and remaining tags by canonical JSON UTF-8. Descending reverses known value order only; non-known states remain last. Every tie is stable.

A result contains:

snapshot, queryDigest, evaluatorVersion,
rows: object<name,Cell>[],
complete: boolean,
coverage: "complete"|"partial",
truncated: boolean,
diagnostics: {code, severity, stage?, row?, message}[],
dependencies: ExactRef[],
definitions: ExactRef[], functions: string[]

complete requires a complete input contract for the requested domain, no unresolved affecting values, no exceeded limits and no truncation. It does not assert knowledge of all facts in the fictional or real world. Count over an access-filtered scope is a count of that scope. Queries with unselected profiles cannot claim production readiness. Definition metadata changes do not affect a query unless explicitly read or selected; diagnostics include which profiles were used. An engine MAY stream rows, but MUST emit final completion metadata or mark interruption; partial output is never a final complete answer.

Semantic cell errors are local. Malformed query AST, unknown stage, duplicate bindings or unavailable requested snapshot reject the query. Resource exhaustion returns explicitly incomplete partial results or a whole-query error according to a caller-selected failureMode ("partial" default, "atomic" optional top-level member). A partial mode can never write state.

6. Graph traversal and dependencies

walk starts from a ref or ordered ref array. It emits start nodes at depth 0, then breadth-first traverses the specified stored field paths in path order, following refs or arrays of refs in array order. Visited key is (work,id,rev); visit each once per input row. as binds {record, depth, via} where via is the exact path of references. maxDepth is required, range 0..64 for portable queries. Encountering an unvisited child beyond the bound marks truncated/incomplete. Cycles terminate through the visited set and are reported informationally. Unavailable nodes produce diagnostics, never fabricate zero outgoing edges as a complete claim.

Impact analysis traverses DECLARED links such as dependsOn, portrays, realises, uses, derivedFrom and evidence. It yields "declared dependants", not a guarantee that every narrative implication was found. An agent can add an attributed reading of further effects, which becomes new searchable evidence.

7. Computations and checks

A core.calculation record stores query, outputDefinition?, purpose and optional parameters. Parameters are an explicit object of Values supplied under the reserved row binding params; they do not perform textual substitution. Parameterised queries must declare expected parameter names; absent values are missing, extra values are preserved but unused and reported. A core.view references a calculation plus presentation preferences; display changes cannot alter the query's meaning.

A core.check records a query and an expectation "all-true"|"none"|"nonempty". For all-true, the query must select a result cell; every row must be known true, and zero rows yields unknown (avoids vacuous creative approval). none passes only for a complete empty result; nonempty passes for a known row but records incomplete coverage if present. Findings have pass/fail/unknown, severity and exact evidence. Only an explicitly selected host operation profile can make a structural check a gate. Creative checks remain advice.

A core.derived result stores inputs, calculation revision, function versions, snapshot, access scope, completeness and output digest. It is a disposable cache. Any changed dependency, definition, function version or access scope invalidates it. A graph absence/aggregate query depends on the source snapshot or a proven collection watermark in addition to enumerated records; a newly added record must invalidate it. A cached reading is not writer canon. Promoting a result to an authored choice uses the normal proposal protocol.

Examples of computable tasks: scene counts by location; character appearances in selected drafts; declared setup/payoff links; claims without evidence; changed source dependants; schedule resource overlaps; exact frame durations; budget sums in one currency; deliverables missing declared assets. Extracting an appearance from unconstrained prose, judging emotional payoff or identifying an implicit continuity error is an attributed interpretation, with a model/version and source anchors. SSL represents that result without promising deterministic artistic judgment.

8. Implementation and scale

An engine can use relational tables for (work,id,rev,kind), JSON field indexes, reverse-reference indexes and text search. The language does not mandate storage. SQL optimisations must preserve null/unknown distinctions, bag ordering, access filtering and exact arithmetic. Search ranking is a separate versioned capability; a semantic similarity score never becomes truth or authority.

Paged queries bind one snapshot and a query digest; opaque cursors cannot drift to later heads. Server budgets include rows scanned, joins, traversed edges, expression depth and output bytes; report the actual limiting dimension. Optional compiled plans are caches of the same query semantics. Future richer programming constructs need termination/cost rules and isolated execution; they cannot change the core's inert data contract.