The Tarski language
A strict Datalog subset for declaring ontologies — relations, rules, decisions, invariants, and the reserved namespaces that make containment structural.
Tarski is Tarski’s ontology language: a strict Datalog subset with Souffle-like syntax and
reserved platform namespaces. It is deliberately not Turing-complete — by staying inside
well-behaved logic fragments, the platform can verify properties of your system that
general-purpose code cannot offer: exact replay, load-time containment checks, cost and
monotonicity analysis.
.ta files serve two roles: ontology files (ontology/*.ta) declare relations, rules,
decisions, invariants, and intents; query files (queries/*.ta) declare parameterized
worldview reads (covered in Declared queries).
What V1 supports and rejects
Supported: positive derivation; bounded recursive positive derivation; stratified
negation; finite non-recursive aggregates; explicit assertions and retractions; pure
helper calls (helper.std.* and app helpers); invariants; intent. derivation.
Rejected: recursive aggregates; unstratified negation; ambient I/O; dynamic rule loading; and unsafe relay paths from fallible evidence into trusted facts or executable intents.
One syntax rule worth knowing early: an aggregate clause terminates a rule body. Put comparisons before the aggregate, or split the rule through a local relation — the parser diagnostic will tell you exactly this.
Rules and decisions
Derivation is classic Datalog: a head relation derives from a body of conditions.
-- fallible parse lands as candidate evidence (relay namespace)
-- promotion is an explicit decision the loader can check
decision.accepted.orders.line(item, qty) :-
candidate.order(item, qty),
menu.bounds(item, max),
qty <= max.
order.line(item, qty) :-
decision.accepted.orders.line(item, qty).
-- out of bounds: route for confirmation instead
decision.requires_review.orders.line(item, qty, "reviewer", "order_desk") :-
candidate.order(item, qty),
menu.bounds(item, max),
qty > max.
The decision relations here are not ceremony — they are the load-checked cut between
fallible evidence and committed truth. Every positive path from candidate.* to a
trusted fact must cross decision.accepted.*, and every path from proposal.* to a
world-facing intent must cross decision.approved.*. The full model, including the
payload-projection rule and common mistakes, is in
Facts, decisions & intents.
Invariants
Invariants are named, fail-closed statements about states the completed model must never contain — checked over the fully materialized model at every fixpoint:
-- the wall: an effect receipt without canonical approval is impossible
invariant pos_only_accepted_lines {
never pos.ticket(item, qty), not order.line(item, qty)
}
Use invariants for impossible committed states and effect-boundary backstops — not for
ordinary proposal denial, which should commit as decision.rejected.* evidence. Common
idioms include exclusivity invariants (a count bounded by one) and budget backstops that
make over-approval structurally impossible.
Reserved namespaces
| Namespace | Owner | Role |
|---|---|---|
source.* |
mappers | Base evidence extracted from observations |
candidate.* |
mappers or rules | Provisional fact-domain evidence |
proposal.* |
mappers or rules | Provisional action-domain evidence |
decision.* |
rules only | Deterministic adjudication (closed outcome set) |
intent.* |
rules only | Committed action requests |
effect.* |
runtime only | Effect lifecycle: queued, in_flight, succeeded, failed, blocked, reconcile_required, conflict |
helper.std.* |
platform | Version-pinned standard helper library |
queryresult.*, proposal.query_requested |
runtime only | Agent read relay families — app declaration is a load error |
access.* |
rules | The account/authorization plane |
Entity sugar
entity blocks are the language’s one sugar construct — a single declaration that expands
deterministically into the full record kit (observation kinds, mapper, current-state
rules, and read queries). See Entities.
entity customer {
key customer_id: text
field name: text
field plan: text index
}
Diagnostics as a contract
Every .ta load failure carries a stable diagnostic code, the source file, line, and
column, the offending construct, and a correct form or concrete remediation. This is a
deliberate product surface: coding agents iterate against diagnostics, so diagnostics are
written to teach. Load-time families you will meet include parse errors (E1040–E1047
for query files), query validation (E2601–E2611), plugin surface violations
(E2701–E2707), and composition admission (E2810, E2811).
Monotonicity, classified
Every rule, relation, and stratum in a loaded ontology is labeled monotone or non-monotone (the CALM boundary), with explicit witnesses naming why — a negation, an aggregate, a retraction, an ungrounded comparison. Monotone logic can distribute and react coordination-free; non-monotone logic needs the stratified fixed point’s ordering.
You can declare the classification you designed around, and the load fails when the computed classification silently disagrees:
[analysis.monotonicity]
expect_monotone = ["workspace.open_task", "task.activity"]
expect_non_monotone = ["task.record", "task.current_version"]
Expectations gate the load only — they never change evaluation semantics. The classification also feeds the composition architecture report; see Workviews, Collectors & modules.