Check that components agree about their relations
Declare a relation once at the field with rel(), acyclic() and inverse(), and have the whole model's edge set checked.
Rendered from
docs/how-to/check-relations.md
in the engine repository, where a correction belongs, and where the
test suite executes every example on this page.
A model can hold field by field and still be wrong as a graph: a dependency loop nobody meant, an edge whose other end never wrote it down. Those are facts about the finished model, so you declare them once, at the field, and the verdict lands when every edge is known.
Declare the relation at the field
The vocabulary below is a trimmed version of
use-cases/12-relations, an ETL
pipeline where jobs feed jobs. Write it as spec.aon:
spec: hide({
Job: {
kind: job
feeds?: rel($.spec.JobShape) & acyclic() & inverse(fedBy)
fedBy?: rel($.spec.JobShape)
}
JobShape: kind: job
})
One conjunction on feeds declares the whole contract. rel(t)
makes the field’s strings checked entity addresses and flows the
endpoint type t into every target; acyclic() and inverse(fedBy)
are the graph atoms: declarations that unification carries along
inert, whose verdict lands at generation. Data documents then stay
plain lists of names. Write the topology as pipeline.aon:
@"./spec.aon"
jobs: { &: $.spec.Job }
jobs: extract: feeds: [path($.jobs.transform)]
jobs: transform: { fedBy: [path($.jobs.extract)] feeds: [path($.jobs.load)] }
jobs: load: fedBy: [path($.jobs.transform)]
Every list is plain: each entry is a
tree address, which
is what rel() checks. Both directions are written out by hand,
because inverse() only checks that the mirror exists; it never
writes the mirror for you. Run the checks:
$ aontu relations pipeline.aon
verdict: pass
Generating the document (aontu pipeline.aon) is unaffected by any
of this, and the lists come out as the same plain strings the author
wrote: rel() checks addresses, it never rewrites them.
A cycle is a located refusal
Suppose a change makes load feed extract, closing the DAG into a
loop. Write the change as cycle.aon, patching both directions in:
@"./pipeline.aon"
jobs: { load:feeds: [path($.jobs.extract)] extract:fedBy: [path($.jobs.load)] }
Every field still unifies (nothing contradicts locally), but the
edge set now violates acyclic(), so generation refuses, with a
located error at an edge on the loop:
$ aontu cycle.aon
[aontu/relation_cycle]: Cannot relate value at path $.jobs.extract.feeds
...
$ echo $?
1
The verb reports the same verdict without generating, and names the nodes the cycle runs through, closing back on the first:
$ aontu relations cycle.aon
verdict: fail
$.jobs.extract.feeds.0 feeds: cycle $.jobs.extract -> $.jobs.transform -> $.jobs.load -> $.jobs.extract
$ echo $?
1
Each verdict line is one finding: the position of the offending edge, the relation, and the detail. Findings are sorted by position, so the report diffs cleanly between runs.
A missing inverse names the missing entry
Add a metrics job that taps the transform output, and forget to
record the feeder on its fedBy. Write it as metrics.aon:
@"./pipeline.aon"
jobs: metrics: fedBy: []
jobs: transform: feeds: [path($.jobs.load) path($.jobs.metrics)]
$ aontu relations metrics.aon
verdict: fail
$.jobs.transform.feeds.1 feeds: $.jobs.metrics does not list $.jobs.transform under fedBy
$ echo $?
1
The finding says exactly which entry to write. Generation refuses the
same model with a located [aontu/relation_inverse_missing] at that
edge.
What to watch for
Acyclicity and inverse consistency are deliberately not lattice
constraints: both are global and non-monotone (one more edge can
make an acyclic graph cyclic), so unification could never hold them
open at all. The endpoint type is different: rel(t) flows t
into each far end at the site, so an edge landing on a wrong-shaped
entity is an ordinary located evaluation error, and the verb answers
verdict: error (exit 4) for a document that does not stand up at
all. Exit codes are otherwise 0 pass and 1 fail, with
--format json for a machine-readable report.
The atoms are specified in Declared
relations, the verb
under aontu relations, and
the live version, with its checks, is
use-cases/12-relations. Once the
edges hold, ask what they connect: Query reachability between
entities.