Why aontu
A code change can pass a service's tests and still violate a rule about the wider system. aontu lets you state those rules as types, defaults, and relations that a program can check.
What the agent does not have
A repository can describe each service without stating which services may call each other, what each tier requires, or which fields every record needs. If those rules are only in prose or a diagram, a build cannot check them. An aontu document makes them explicit.
A type checker checks the rules expressed in its types; a JSON Schema validator checks the supplied schema. aontu adds a shared definition for entities and their relations. You can require an owner for every service, constrain its dependencies, and refuse a cycle in the graph.
Types, defaults, and data use one notation
In aontu a type is a value that admits many values, a default is a value that yields to a more specific one, and a fact is a value that is already specific. All three are the same kind of thing, so one document holds the vocabulary and the data together and the engine checks them in one pass. Say a key twice and the two statements combine:
deploy: region: "eu-west-1"|"us-east-1"
deploy: replicas: integer & min(2)
deploy: region: "eu-west-1"
deploy: replicas: 6 evaluates to
{
"deploy": { "region": "eu-west-1", "replicas": 6 }
}
Set replicas to 1 and the document fails
to evaluate because min(2) still applies. The constraint
and the concrete value are checked together.
Check relations between entities
Services depend on services, jobs feed jobs, and records reference
records. In aontu, an entity's identity is its path in the document.
A relation can require typed endpoints, an acyclic graph, or a
corresponding edge on the target. This example keeps the vocabulary
under hide(), so it is checked but omitted from output:
spec: hide({
job: {
kind: job
downstream?: rel($.spec.shape) & acyclic() & inverse(upstream)
upstream?: rel($.spec.shape)
}
shape: kind: job
})
jobs: { &: $.spec.job }
jobs: extract: downstream: [path($.jobs.transform)]
jobs: transform: {
upstream: [path($.jobs.extract)]
downstream: [path($.jobs.load)]
}
jobs: load: upstream: [path($.jobs.transform)] evaluates to
{
"jobs": {
"extract": { "downstream": ["$.jobs.transform"], "kind": "job" },
"load": { "kind": "job", "upstream": ["$.jobs.transform"] },
"transform": { "downstream": ["$.jobs.load"], "kind": "job",
"upstream": ["$.jobs.extract"] }
}
}
Remove the edge from load back to transform.
The refusal identifies the missing inverse edge and the declaration
that requires it:
[aontu/relation_inverse_missing]: Cannot relate value at path $.jobs.transform.downstream
This relation declared inverse(name), and an edge has no mirroring
edge under that name: A relates to B, and B does not name A back.
Cannot relate value: path($.jobs.load)
--> pipeline.aon:4:18
2 | job: {
3 | kind: job
4 | downstream?: rel($.spec.shape) & acyclic() & inverse(upstream)
^ value was: path($.jobs.load)
5 | upstream?: rel($.spec.shape)
6 | } The inverse-edge requirement is part of the schema, so validation checks it whenever you evaluate this model.
One definition, every environment
An environment can reuse a base definition and add its own values. Here, production sets the replica count and inherits the log level:
spec: hide({
service: { owner:string replicas: *2|integer log: *"info"|string }
})
base: { &: $.spec.service api:owner:"platform" }
prod: $.base & { api:replicas:6 } evaluates to
{
"base": { "api": { "log": "info", "owner": "platform", "replicas": 2 } },
"prod": { "api": { "log": "info", "owner": "platform", "replicas": 6 } }
} Changing a default affects environments that have not specified that value. A conflicting overlay cannot replace a fixed value. Use why to see which statements contributed to a result, with their filenames and line numbers.
Use validation results in a program
vet reports
valid, invalid, incomplete, or error, with a distinct exit code for each.
A repair loop can use the verdict and error class
to choose its next action. breaking checks compatibility
with an earlier schema, and subsume checks whether one
definition admits another. aontu mcp provides the same
reports locally over stdio.
What it costs
aontu has no user-defined functions, clock, or randomness. Evaluation uses bounded recursion and configured budgets. The trust contract specifies the conditions for termination and deterministic output, including host inputs and file access. Use a host language for behaviour beyond those limits.
Unification combines compatible statements into a more specific value and reports contradictions. Statement order does not give one source priority over another.
Start with the tutorial, or run the use cases to check models such as a service catalog and an event-contract registry.