aontu

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.