Ontology
This is the design the organisation and domain model were written to, and it is kept because it records why each part has the shape it has: what was considered, what was chosen, and what the choice costs. For how to use it, read USER-GUIDE → Projects and USER-GUIDE → Domain model. For the module format, see REFERENCE → Ontology modules.
An agent deployed into an organisation needs two things it cannot get from a collection of documents. The first is a description of the organisation it works in: how it is divided, and what each part is for. The second is a description of the domain it works on: the things the organisation deals with, the classes they belong to, and the relations between them.
The first is structure and direction. It is the scope Milieu already runs in, extended from a flat list of projects to a tree of units, each with a charter. The second is the ontology graph. Knowledge documents, memories, skills, tools, gateway routes, and the agents themselves are part of neither. They attach to the graph, and the graph is read within the structure.
Contents
- 1. What the existing stores cannot answer
- 2. What the ontology is, and is not
- 3. The organisation
- 4. The shape of the graph
- 5. Identity
- 6. Declaring the domain
- 7. Attachments: knowledge, memory, tools, agents
- 8. Provenance and trust
- 9. Layers
- 10. What the model is used for
- 11. Storage and query
- 12. Surfaces
- 13. Worked example: the contact centre
- 14. An installation that declares nothing
- 15. Open design questions
1. What the existing stores cannot answer
Milieu holds four bodies of state, and each is retrievable on its own terms. Knowledge is Markdown, found by keyword and by meaning. Memory is a set of small notes, found through an index of their descriptions. Capability is a skill’s declared tool list intersected with an agent’s grants. Coordination is a roster of agents, their projects, and the messages between them.
Each store answers questions about itself. None of them answers a question about the business:
- Which policies govern a refund on a lapsed home-cover policy in Europe, the Middle East, and Africa (EMEA), and which of them have been superseded?
- If an agent has to reason about a claim, what is a claim: what does it have, what is it attached to, and which system holds the live one?
- Who is accountable for pricing, and does that agent have a route to the system that owns it?
- This reply cited the four-hour credit threshold. Did that come from a policy article, from an agent’s own note, or from a number a tool returned?
Or about the organisation:
- The ACME account sits inside customer operations. What is customer operations for, and which of its objectives is the ACME work meant to advance?
- An escalation rule holds for every team in the department. Where is it written once, so that each of those teams reads it and none of them keeps a copy?
- Which supplier ships replacement routers, who owns that relationship, and how does an agent reach the supplier’s system?
A person who already knows the answer can give it. A keyword search for “refund” returns documents that contain the word “refund”. It does not know that a credit is a kind of refund, that the ACME service-level agreement (SLA) overrides the house threshold, or that the number in the reply came from a note one agent wrote for itself six weeks ago. Before units existed, a project was a flat workspace with no parent to inherit from and no statement of what it was for.
Better retrieval doesn’t close this gap. The organisation’s vocabulary (the classes of thing it deals with and how they relate) and its shape (how it is divided and what each part is for) existed only in prose, and prose cannot be traversed.
2. What the ontology is, and is not
The ontology graph holds the domain knowledge. Its nodes are the things the organisation works with, and its edges are the relations between them. People who know the business write it, it is reviewed like a policy, and it is versioned like code.
The organisation is the context that knowledge sits in. Its units (the
organisation itself, its departments, its projects) and each unit’s charter of
vision, mission, and objectives are defined in §3. They
are not written in the graph. Structure is the scope a run executes in, so it
lives where scope already lives, in project.toml, and the graph reads it back
as derived nodes, so one definition is read in two ways and the two cannot
disagree.
Everything else Milieu owns attaches to the graph:
| Attaches as | Meaning | |
|---|---|---|
| Knowledge | documents | this article is about that domain thing |
| Memory | notes | this agent’s note is about that domain thing |
| Skills / tools | acts_on | this capability operates on that class |
| Gateway routes | resolves | live instances of that class live behind this route |
| Gateway routes | exposes | this route is how that outside party is reached |
| Agents | accountable_for | this agent owns that part of the domain |
| Projects | scopes | this workspace is about that part of the domain |
| Runs | touched | this piece of work consulted that domain thing |
The domain model is the stable, reviewed, slowly changing part. The attachments are mostly derived from state Milieu already keeps, and can be rebuilt. An ontology whose nodes were agents and skills would be a diagram of Milieu, which CONCEPTS §17 already provides and which no agent needs at run time.
Units are the one place that division looks unclear. A department and its objectives belong to the business side of it. A project is both a business unit and a Milieu workspace, and for that reason it is one object: the thing a run is scoped by and the thing the organisation chart names are the same record.
What the graph is not:
- It is not a system of record. It does not hold customers, claims, invoices, or tickets. Those live in the systems that own them and are reached through a gateway route. A copy in the graph would be stale by the time it was read, and would be a compliance surface nobody asked for.
- It is not a human resources (HR) system. Units are the shape work is scoped by. People, reporting lines, and headcount stay where they are.
- It is not a replacement for search. Keyword and vector search are unchanged. The graph tells retrieval what to reach for, but it does not rank prose.
- It is not a Web Ontology Language (OWL) reasoner. See §11.
- It is not writable by agents. An agent that can add edges is editing what it will be shown next time. The argument is the same as for memory writes, with a wider reach: a memory shadows one file, and an edge changes what an entire class retrieves. The charter is under the same rule for the same reason. See §8.
3. The organisation
This part of the design changes how Milieu scopes a run. It needs no graph and is useful on its own, and the graph reads what it declares as derived nodes.
One installation, one root
Global is the organisation that runs the installation. The global layer is already where the organisation’s own assertions live, and it is in every run’s union. It is the root of the unit tree and holds the organisation’s charter.
Two independent organisations are two installations. The link between two companies is an agent-to-agent (A2A) route and a gateway credential (CONCEPTS §15), which is the right trust boundary for that relationship.
A provider running Milieu for several clients is one installation, shaped by the tree: global is the provider, and each client is a top-level unit that controls what its subtree reads. A client’s unit carries its own charter, instructions, and terms, and shadows any global article, class, or alias it needs to differ on. This is the existing shadowing rule applied at the client’s unit, with no further mechanism.
Shadowing cannot subtract. A client unit can replace a global article but cannot hide one it has no replacement for, and department context and the provider’s own mission line accumulate, so a client cannot shadow them. Inherited graph edges are the one thing a client can remove, and only by an explicit, recorded retraction (§9). A provider should therefore keep global thin, holding what is true for every client and nothing else, and put its internal material in a unit of its own, outside every client’s subtree:
global the provider: what holds for every client, and no more
├─ house provider-internal runbooks, pricing, the provider's own agents
├─ client-a client A's charter, instructions, terms and overrides
│ └─ …
└─ client-bUnits: a project with a parent
A department is a project with children. project.toml has two fields for it:
# var/projects/acme/project.toml
code = "acme"
name = "ACME account"
parent = "customer-ops"
level = "account" # a label; nothing branches on it
members = ["mira"]There is no separate kind for a department. A department needs what a project already has (a roster, instructions, a knowledge layer, a memory layer) and nothing a project lacks. Two kinds with the same four parts would drift, and the first place they would drift is the membership gate, which is the one place they must not.
level is only a label. Organisations disagree about depth and about names:
division, department, and team in one; practice and engagement in the next; a
flat list of accounts in a third. level is printed by project tree, by
lint, and in the charter heading. No code branches on it, so no organisation is
forced into another’s shape.
The parent is a field in project.toml, so var/projects/<code>/ stays flat
and a reorganisation is a one-line edit: every project:<code> identifier and
every path an audit row or transcript names survives it, and codes stay unique
across the whole tree. Labels change and identifiers do not.
§5 states the same rule for the graph.
The project service refuses a parent that does not exist, a cycle, and a chain
more than six projects deep, and lint reports each of them. A move that would
put any project below the moved one past the limit is refused as well.
project rm removes a unit, and refuses one with children until they have been
given another parent: a child left behind would read a chain through a unit that
is gone, and removal can’t infer which department it belongs to instead. It refuses a layer holding knowledge or memory too, unless --force,
because that content was authored in place and nothing snapshots it. Oversight
grants naming the unit go with it, because they live on each agent, not
in the project. The audit log is untouched: it is append-only, and the runs that
named the unit still happened. Removal stays a command you run by name:
apply --prune removes no unit, because dropping a project from a manifest
should not destroy the layer it declared as a side effect. It disables the unit instead:
no run or session starts in it, its layer stays readable and takes no writes, and declaring it
again enables it.
The scope chain
A run --in acme reads the whole chain: the agent, the unit, each ancestor
nearest first, then global.
Union and shadowing work as they do for a single project, with more than one unit layer in the middle: every layer in the chain is merged, and on a path or name clash the most specific wins. A department article is the default for every unit under it, and a unit shadows it where it must. The chain applies to knowledge, memory, instructions, the charter, and the graph.
The knowledge provider and the memory service append every layer in the chain. The gate each takes returns the chain, so the policy lives in one place, the project service’s access check.
Membership: reads go up, never down
Three rules apply:
- Acting in a unit needs direct membership of it.
--in acmerequiresmiraon the ACME roster. - A member of a unit reads every ancestor’s layers. A department layer is written once and read by everything under it.
- A member of a department does not read the units below it. Reading down is a grant someone decides on. For more information, see Oversight.
Writes are narrower. A write targets one layer, the run’s own unit: a memory
saved during an --in acme run lands in ACME’s layer, never in customer
operations’. Writing to the department layer means acting --in customer-ops,
as a member of it. A wider layer is a wider audience, so writing to it is kept
a separate act.
Adding an agent to a unit therefore exposes it to every ancestor’s knowledge and
memory. project show prints what a project’s members read, and
project set --parent and apply print the chain each affected project now
reads.
Oversight: reading down, by grant
Some agents’ job is the units below a department: a quality reviewer across every team, an auditor across every account. Listing them on every child roster is the copying that units exist to remove, so an agent may instead be granted oversight:
# var/agents/vera/agent.toml
role = "auditor"
oversees = ["customer-ops"]The grant lets the agent scope a run into the named unit or any unit below it,
--in acme, without being on that unit’s roster, and read that unit’s chain as
a member would. Three limits distinguish it from membership:
- It is read-only. A memory the overseer saves during such a run lands in its own agent layer, never in the unit’s.
- It reaches one unit at a time. There is no view that unions sibling units:
siblings have no shadowing order, and a merged read of two accounts would
answer with whichever copy of
articles/sla.mdcame first. - It passes through the same gate. The project service checks the grant beside the roster, so the policy still lives in one place.
The grant lives on the agent because it is a statement about the agent’s job,
made once. It is also the kind of grant that is easy to
hand out and hard to notice, so it is reported where a reparent is:
project show lists every overseer that reaches a unit alongside its members,
and apply prints what a grant reaches when it changes and, when a unit is
created beneath an overseen one, the overseers it is visible to.
Reorganising
Changing a unit’s parent changes what every member of that unit, and of every
unit below it, reads on their next run, with no file moved. Nothing else makes
that visible, so apply prints the old and new chain for every affected unit,
and the change is recorded like every applied change
(§8): an audit row naming the unit and its old and new
digest. A reorganisation is the edit in this design with the widest reach, and
it is reported accordingly.
The charter: vision, mission, objectives
Every unit may carry a charter.yaml beside its project.toml. The
organisation’s is etc/charter.yaml.
# etc/charter.yaml
name: Example Broadband
vision: Nobody has to chase us for an answer.
mission: Keep households connected, and put it right quickly when they are not.
objectives:
- id: first-contact
statement: Resolve most contacts at the first touch, within policy.# etc/projects/customer-ops/charter.yaml
mission: Own every customer contact from first message to resolution.
objectives:
- id: one-touch-80
statement: Resolve 80% of contacts without a handoff.
advances: [global/first-contact]
until: 2027-03-31Four fields, all optional:
| Field | Meaning |
|---|---|
vision | where the unit is going (usually only the organisation states one) |
mission | what the unit is for, in one sentence |
objectives[].id, statement | what the unit is trying to achieve now |
objectives[].advances | the objectives, in an ancestor or global, this one serves |
objectives[].until | when it lapses |
Goals, objectives, OKRs (objectives and key results), and key results are one
idea at different levels, and one field and a tree carry all of them. advances
is the tree. It may name only an ancestor’s objective, because the claim it
makes is that this is why the work below exists, and lint can check that.
There is no field for a measure, because a number in a charter file that
nothing measures would be decoration. Where an objective is measured, the
measure lives with the data.
Connecting an objective to the eval suite or audit report that evidences it is
out of scope for this design. until is the only field of an objective that
anything checks.
The charter is YAML because advances is a reference that has to resolve and
until is a date that has to lapse, and prose can do neither.
The narrative, such as the strategy paper or the reasoning behind an objective,
belongs in knowledge, where it is searched like everything else and can say it
is about: the objective.
The charter lives beside project.toml, outside every agent’s writable home,
because an agent able to edit the objectives it is steered by would be editing
its own instructions.
How the charter reaches a run
The charter is rendered into the system prompt on every run, as a head layer between the agent’s identity brief and the recall index.
The position follows the rule the system prompt already obeys: head layers are ordered by how often they change, so a provider’s prefix cache can reuse what did not. A charter changes on a planning cycle, more rarely than recall, memory or instructions. It varies by scope where the brief varies by agent, so it goes after the brief and leaves the per-agent prefix intact.
What a run --in acme sees:
# Where this work sits
Example Broadband — Keep households connected, and put it right quickly when
they are not. Vision: nobody has to chase us for an answer.
customer-ops (department) — Own every customer contact from first message to
resolution.
acme (account) — Keep ACME's sites inside their one-hour SLA.
Objectives for acme:
- acme/one-hour-credit: Credit an SLA breach within the hour.
advances customer-ops/one-touch-80 → global/first-contactFive rules keep it small:
- Vision and mission for every unit in the chain, outermost first, one line each.
- Objectives for the run’s own unit only, each followed by its
advancespath. Ancestors’ objectives appear on that path and nowhere else. A department with twelve objectives would otherwise put twelve lines into every run in every unit below it, and the path is the part that tells the model why its own objective matters. - Lapsed objectives are left out and reported by lint, as a stale news item is labelled stale.
- A byte cap. A run is told at most 3 KB of charter, terms, and department context together, the charter first, and the rest is cut. Lint reports a chain that composes past the limit, ahead of any run.
- An unscoped run sees the organisation’s vision and mission only.
Inherited instructions: the department context
A department’s instructions.md reaches every unit below it by default. It is
rendered as a head layer of its own, separate from the unit’s instructions and
headed by the ancestor’s level and name:
# Department context: Customer operations
Escalate any contact that mentions a regulator to arun, whatever the account.
# Instructions
(the unit's own instructions.md, as before)Each ancestor gets its own section, outermost first, so the unit’s own
instructions come last and closest to the skill body, where the most specific
word is nearest the task. An ancestor with no level is headed
Context from <name>.
The layer sits immediately above the unit’s instructions in the system prompt, so the two read together. The inherited sections count toward the same byte cap as the charter, and lint reports the composed size in every unit below a department that passes it: the author is writing for all of them, and the report shows them what it costs.
4. The shape of the graph
There are four kinds of node, and only the first two are the ontology proper.
Classes are the kinds of thing the domain has: claim, policy, customer,
region, product-line. A class declares what identifies its instances and,
where live instances exist elsewhere, which route resolves them.
Instances are individual things, of two sorts:
- Reference instances are stable, few, and named in prose:
region/emea,product-line/home-cover,sla-tier/gold,supplier/parcelco. These belong in the graph. They are the nouns policies are written about, they change slowly, on a governance cycle, and an article that mentions “EMEA” needs something to point at. - Operational instances are the transactional population:
claim/4471,ticket/88213. These do not belong in the graph. The class carries akeyed_byand aresolved_viaroute, and an agent that needs claim 4471 fetches it. The graph defines the class, and the route answers which instances exist.
Organisational nodes are the units of §3 and the
objectives in their charters. They are derived from project.toml and
charter.yaml and are never written in an ontology file.
Attachment nodes are the handles Milieu’s own objects are known by: a knowledge concept id, a memory name, an agent code, a route name. They exist so an edge has something to point at. They are derived from state that already exists and are never written here.
Solid edges are the domain, written by the business. Dashed edges are attachments, mostly derived by Milieu. The structure of interest is the domain classes and instances. The attached nodes are bookkeeping.
Structural predicates
Milieu ships six structural predicates, the two organisational ones that follow, and the attachment set of §7. The installation declares everything else in the domain.
| Predicate | Subject → object | Transitive | Notes |
|---|---|---|---|
subclass_of | class → class | yes | credit-note subclass_of refund |
instance_of | instance → class | no | reference instances only |
part_of | node → node | yes | mereology: region/emea part_of region/global, and also unit → parent unit |
same_as | node → node | symmetric | reconciles the same thing named twice |
supersedes | node → node | yes | lifts the existing Open Knowledge Format (OKF) key to the graph |
related_to | node → node | no | the untyped fallback |
related_to is the one predicate milieu lint reports by volume: a model
given a free-text relation slot will use it for everything, and a graph that is
mostly related_to is a tag cloud.
Organisational predicates
These are derived, never declared in an ontology file, and read from
project.toml and charter.yaml on every view.
| Predicate | Subject → object | Transitive | Derived from |
|---|---|---|---|
part_of | unit → unit | yes | parent in project.toml (the structural predicate, reused) |
pursues | unit → objective | no | the objectives in the unit’s charter.yaml |
advances | objective → objective | yes | advances: in charter.yaml |
part_of is reused for units. A unit is part of its parent in the same sense
that region/emea is part of region/global, and one transitive predicate
means one traversal answers both “everything in EMEA” and “everything in
customer operations”.
Domain predicates
An installation declares its own, each with a subject class, an object class, an optional inverse, and optional transitivity:
relations:
- name: filed_against
from: claim
to: policy
inverse: has_claim
- name: governs
from: policy
to: product-line
transitive: falseBecause the ends are declared, milieu lint can reject
refund filed_against region/emea before it widens what a query returns.
Parties outside the organisation
Suppliers, partners, regulators, and a customer account large enough to be named in contracts. They are reference instances (few, stable, and written about), so the design already has a place for them. It needed a link from a party to the interface that party offers.
A class’s resolved_via says where live instances of the class are fetched. A
supplier’s API holds no population of suppliers. It is how one supplier is
reached, so resolved_via does not fit it. The link is on the instance, and it
comes from the route:
# var/global/ontology/parties.yaml
classes:
- name: supplier
description: An organisation we buy goods or services from.
instances:
- class: supplier
key: parcelco
label: ParcelCo
description: Ships replacement routers to customers, next working day.
aliases: [the courier]# the gateway route, in the manifest
- name: parcel-tracking
prefix: /parcelco
upstream: https://api.parcelco.example/v2
auth: { kind: bearer, tokenEnv: PARCELCO_TOKEN }
ontology-instance: supplier/parcelco # derives: route:parcel-tracking exposes domain:supplier/parcelcoIt is declared on the route because the route is where you already decide what exists and which credential it carries. An ontology file that could name a route would be a second place a route could be invented.
The internal definition of a party is the instance and what attaches to it: the
contract article that documents it, the agent accountable_for the
relationship, the units that scopes it. The external definition is what the
route’s upstream says about itself, which for a Model Context Protocol (MCP) or
A2A route is the tool list or agent card the gateway already fetches. The two
are kept apart. What the organisation believes about a supplier and what the
supplier advertises are different claims, and merging them would let a vendor’s
description become policy.
5. Identity
Every node has a stable identifier. Labels change and identifiers do not, because an edge that survives a rename is the reason for storing edges at all.
Identifiers are compact URIs (CURIEs): a short prefix, a colon, and a path.
Milieu reserves the prefixes for its own objects. The domain gets one prefix the
installation chooses, defaulting to domain:.
Every identifier is lowercase. Each path segment uses the character set a
project code already does, [a-z0-9][a-z0-9_-]*, and a class of more than one
word is kebab-cased: domain:sla-tier, domain:product-line/home-cover. One
case everywhere means an identifier typed in frontmatter, a manifest, or a CLI
argument either matches or does not, with no second spelling to reconcile, and
it reads the same as the project:, agent: and knowledge: identifiers
beside it. What a person reads is the label, which keeps whatever capitals it
needs.
| Form | Example | Comes from |
|---|---|---|
domain:<class> | domain:claim | declared |
domain:<class>/<key> | domain:region/emea | declared (reference instances only) |
term:<slug> | term:truck-roll | declared (definition-only terms, §6) |
org:root | org:root | the organisation, labelled with the global charter’s name |
project:<code> | project:acme | project code |
objective:<unit>/<id> | objective:customer-ops/one-touch-80 | a charter, with global for the organisation’s |
knowledge:<layer>/<concept-id> | knowledge:global/articles/refund-policy | knowledge.ConceptID, layer-qualified |
memory:<layer>/<name> | memory:agent/mira/refund-window | memory name and layer |
agent:<code> | agent:mira | agent code |
skill:<agent>/<name> | skill:mira/respond | skill key |
route:<name> | route:claims-api | gateway route name |
tool:<name> | tool:knowledge | built-in or granted command |
run:<seq> | run:8231 | the audit record’s sequence number |
Four consequences follow. A knowledge concept id is layer-qualified here even
though the concept id itself is not, because the global and project copies of
articles/refund-policy are different documents that may attach to different
things. A skill identifier omits the digest: an edge saying which skill acts on
claims is about the skill as a whole, and the digest stays in the audit row and
the transcript, where the question of which build ran is asked. A project:
identifier does not encode its parent, so it survives a reorganisation. For this
reason, §3 keeps the directory flat. An objective
identifier is unit-qualified, because two units may each have an objective
called first-contact and mean different things by it.
6. Declaring the domain
The domain is written, reviewed, and applied like everything else, through two entry points that carry the same data.
In the manifest, kind: Ontology, reconciled by milieu apply alongside
agents, projects, and knowledge:
apiVersion: milieu.dev/v1
kind: Ontology
metadata: { name: insurance }
spec:
# no project: the house model, in the global layer
sources: [./global/ontology]On disk, one YAML file per module under the layer’s ontology/ directory, so
the model diffs in review like any other written file:
# var/global/ontology/claims.yaml
classes:
- name: claim
label: Claim
description: A request for payment under a policy.
subclass_of: case
keyed_by: claim_id
resolved_via: route:claims-api # live instances live here, not in the graph
aliases: [loss notification, FNOL]
- name: refund
description: Money returned to a customer.
aliases: [credit, credit note, chargeback]
relations:
- { name: filed_against, from: claim, to: policy, inverse: has_claim }
- { name: settles, from: refund, to: claim }
instances: # reference instances only
- { class: region, key: emea, label: EMEA }
- { class: sla-tier, key: gold, label: Gold, part_of: sla-tier/standard }name and key are identifiers and follow the lowercase rule of
§5. label is display text and defaults to the name. claim
with label: Claim is how a class reads well in both places.
aliases is the field that turns “the customer wants a credit” into a hit on
domain:refund, and the one an installation keeps adding to.
Everything is optional. A class with a name and nothing else is a legal class, and an installation that declares six classes and no relations still gets the governance queries of §10 out of it.
Vocabulary
An organisation’s vocabulary is larger than its classes. It has words for
things the graph models (“the courier”) and words for things it does not
(“truck roll”, “P1”). A terms: section holds both:
terms:
- { term: the courier, means: domain:supplier/parcelco }
- { term: P1, means: domain:priority/p1 }
- { term: truck roll, definition: Sending an engineer to the customer's premises. }A term with means compiles into an alias on the node it names. It is the
mechanism aliases already provide, declared where someone writing a glossary
would look for it. A term with only a definition becomes a node of its own,
term:<slug>, for a word the organisation uses that no class covers.
Terms are layered along the scope chain like everything else, and shadow by
term: ACME’s “credit” can mean a service credit whereas the house’s means a
refund, and a run --in acme reads ACME’s.
The house glossary reaches the model through retrieval and the graph tool:
it has hundreds of entries, nearly all of them irrelevant to any one run, so it
is left out of the prompt. The run’s own unit’s terms are the exception: they
are few, they are the words a model cannot guess, and they are rendered beneath
the charter within its byte cap.
7. Attachments: knowledge, memory, tools, agents
Milieu ships eight attachment predicates. The set is closed: an installation can add domain predicates and no attachment predicates.
| Predicate | Subject → object | Declared where | Or derived from |
|---|---|---|---|
documents | knowledge → domain | about: in OKF frontmatter | None |
notes | memory → domain | about: in memory frontmatter | None |
acts_on | skill → domain class | acts-on: in skill frontmatter | None |
resolves | route → domain class | ontology-class on the route | None |
exposes | route → domain instance | ontology-instance on the route | None |
accountable_for | agent → domain | ontology.owns in agent.toml | None |
scopes | project → domain | ontology.scope in project.toml | None |
touched | run → domain | None | the audit log and the transcript |
Each has an inverse, which is how the graph is usually read: documented_by,
noted_by, acted_on_by, resolved_via, exposed_by, owned_by,
scoped_by, touched_by.
Three notes on how they are written:
about: is OKF frontmatter, and a legal key: the Open Knowledge Format requires
consumers to tolerate unknown keys, which is the licence
supersedes already relies on. It keeps the claim
beside the prose that makes it, and it may name an objective as readily as a
class:
---
type: article
title: Refund policy
about: [domain:refund, domain:region/emea]
---touched is not stored. Every run already writes an audit row and a transcript
recording which knowledge it read, which memory it recalled, which tools it
called, and which peers it asked. Materialising that into edges would grow the
graph without bound and duplicate an append-only record that is already the
authoritative one. graph why is therefore a join: the log says what the run
read, and the graph says what those things are about. The graph stays the size
of the domain, and the history stays in the log, where it is hash-chained.
Attachments never widen authority. An accountable_for edge states who owns a
part of the domain, and an exposes edge states where a party is reached. What
an agent may call is still the intersection of the skill’s declared tools and
the agent’s permissions, checked at invocation, as
ARCHITECTURE §7 describes. The
graph can say an agent should own pricing and report that it holds no route to
the pricing system, but it cannot hand it one.
8. Provenance and trust
Every node and every edge records where it came from. There are three origins,
and all three exist. Inference, including graph infer, is held to the
following rules.
| Origin | Written by | Rebuildable | Example |
|---|---|---|---|
| declared | a person, in a reviewed file | no | claim filed_against policy |
| derived | Milieu, from state it already holds | yes | agent:mira accountable_for domain:refund, read out of agent.toml, and project:acme part_of project:customer-ops, read out of project.toml |
| inferred | a model, from the corpus | yes, and expires | domain:policy/house-refunds governs domain:product-line/home-cover, read from the policy that says so |
Trust reuses the ladder knowledge already has: unverified,
machine-confirmed, and human-reviewed. A declared edge is human-reviewed by
construction. A derived edge is machine-confirmed: nothing judged it, but
nothing guessed either. An inferred edge starts unverified and stays there until
a person confirms it.
Units and charters are derived for the same reason accountable_for is: the
graph reads them from files a person wrote elsewhere. The review happens on
project.toml and charter.yaml.
Three rules contain inference:
- An inferred edge never outranks a declared one. Where both exist for the same subject and predicate, the declared edge wins and the inferred one is dropped.
- Inferred edges expire. Each carries an
until(two weeks by default), andgraph sweepremoves the lapsed ones. A read leaves an expired edge out even before the sweep removes it. An inference made against a document that has since been rewritten should not persist because nobody revisited it, and nothing here notices the rewrite. - An agent may propose an edge, but it may not write one. The distinction is the subject of the Proposals section. It is stricter than memory, where an agent writes its own layer directly: a bad memory misleads one agent, and a bad edge changes what every query on that class returns, including, if the graph is trusted for retrieval, the query that would have surfaced the correction.
That last rule is the prompt-injection boundary. A document an agent ingested can contain a sentence written to be read as a relation. If inference runs over ingested content and its output is trusted for retrieval, the content is steering retrieval. Expiry, the declared-wins rule, and the review gate in Proposals bound that risk. None of them removes it, so if you turn inference on over untrusted sources, you accept that trade.
Versioning by digest
Everything milieu apply reconciles carries a digest and a timestamp, and the
organisation and its domain model are no exception. Every unit, charter, and
ontology module has an applied history: each digest it has had, when it was
applied, the audit row that recorded it, and a snapshot of what it said.
That is all of versioning, and it is the stance skills already take: digests are compared, and there is no version number to forget to bump, because the record is a digest of the content itself.
The graph refines it one level down. Each node carries a digest of its
definition (description, subclass_of, keyed_by, a relation’s ends, an
objective’s statement) and the time that digest last changed. A new alias
changes only what links to a node, so it is left out of the digest. Three uses
follow:
- What a run read, and whether it has moved since. A run records
context_digest, the digest of the charter, terms, and department context it was told, beside the skill digest it already records.graph why run:8231resolves the run against the snapshots: where a node has been redefined since, what it meant is read from the declaration in force at the time, not from the view as it is now, and the change that moved it is named beneath. The label shown is today’s, because it is how a reader finds the node again and is not part of the definition a digest covers. A node whose snapshots retention has swept says so instead of showing today’s meaning. - Changed since verified. A knowledge document
about:a node whose definition changed after the document’s newestverifiedentry is flagged by lint and labelled in results the way a stale document is, until someone verifies it again. The same rule holds inside charters: an objective whose statement changed flags every objective thatadvancesit. A class whose meaning shifted no longer re-points everything attached to it without notice. Instead, it produces a re-review list, with a name on each line. - History for review.
graph history <uri>lists the definitions a node has had, when each was applied and by whom, one line each time its meaning moved.
Proposals
An agent that reaches for a relation the model does not have is the best signal an ontology gets: it is grounded in work someone asked for, which an offline pass that generates plausible edges nobody needed cannot claim.
The built-in graph tool therefore has a propose verb beside its five read
verbs, gated by a permission of its own, graph(propose:*), because suggesting
what everyone reads is a different right from reading.
A proposal is not an edge, and never becomes one by ageing. It lands in a queue no read path traverses:
$MILIEU_HOME/var/ontology/proposals/That is outside every agent’s writable home, for the same reason sessions and eval cases are: an agent that can edit its own proposals after an operator has seen them is not making a proposal. The built-in tool runs host-side, so the sandbox never needs write access to that directory.
A proposal carries the triple and the evidence for it:
| Field | Why it is there |
|---|---|
| subject, predicate, object | the claim |
rationales | each distinct reason given, in the agents’ own words |
proposers | each proposing run’s agent, skill, and skill digest, and when |
sources | the nodes the runs had already read when they proposed |
count, first_seen, last_seen | how many distinct runs have proposed the same triple |
status, reason, decided_by, decided_at | the decision, when someone makes one |
The evidence fields attribute each claim, and count is the field you sort by:
a relation twenty independent runs reached for is a hole in the model, and
identical triples collapse into one row.
Four rules keep the queue reviewable:
- A proposal is type-checked at the moment it is proposed. One that violates a declared subject or object class is refused in the run, with the reason returned to the model. That keeps the queue clean and tells the model something true about the domain at the moment it is wrong.
- A run may propose three edges. An unbounded propose verb is a denial-of-review attack available to any confused model.
- Rejection is remembered. A rejected triple is suppressed, with a reset for when the model has moved on. Without this, the queue fills with the same three declined suggestions and stops being read.
- Acceptance produces a diff.
graph propose acceptwrites the triple into the layer’s declaredontology/file, where it is reviewed, committed, and applied like everything else. The posture is the one every suggesting tool here takes: a tool that widens a model on evidence it generated hands a person the change to make and doesn’t make it itself.
Proposals are for the domain. An agent cannot propose a unit, a parent, or an objective: those are decisions about how the organisation is run, and an agent’s suggestions about its own reporting line are not a useful signal.
graph infer is the batch producer for the same queue and the same review path.
It is the weaker of the two producers, because in-run proposals are grounded in
work that needed the relation and corpus inference is grounded only in the
corpus. It runs only when you run it, never on a timer, because these rules exist to
keep a model’s writes into the graph visible.
It reads a document at a time and may name only what the view already declares: an existing node, an existing predicate, nothing new. This check enforces the prompt-injection boundary: a candidate naming anything else is dropped before it reaches the queue, whatever the document said and however confidently the model reported it. A pass run twice proposes nothing the second time, because its own edges are in the view it checks against.
9. Layers
The graph is layered like knowledge and memory, and along the same scope chain: global, each ancestor unit, the unit a run is scoped to, and the agent, read as a union, most specific first.
- Global holds the house ontology. Almost everything lives here.
- A department holds what is true across the units under it: its own vocabulary, the suppliers only it deals with, the classes only it needs.
- A unit holds what is true within one engagement. ACME calls a credit a
goodwill payment, and that alias belongs in
project:acmealone. - Agent holds what attaches through the agent’s own knowledge and memory, and no other agent’s, and the edges inference has added to it. It is the only layer inference writes to, so that an agent’s guesses do not become the organisation’s model by accumulating: they reach that agent’s runs the way its own memories do, and the queue is the only way further.
Union is a merge of every layer in the chain. Shadowing needs a rule of its own, because it was defined for documents, where a path clash has an obvious winner, and edges have no path:
- Node declarations shadow by identifier, nearest first. A unit redeclaring
domain:refundreplaces the label, description, and aliases its department and the global layer give it, for readers in that unit. - Terms shadow by term, the same way.
- Edges accumulate unless retracted. A more specific layer adds edges and never removes an inherited one as a side effect. It may remove one explicitly.
Nodes and terms need no retraction, because redeclaring them already replaces them. Edges do: a unit where an inherited relation does not hold, such as a client whose contract replaces the house refund policy, otherwise has no way to say so. A retraction is a declared line in the unit’s own ontology file:
# var/projects/client-a/ontology/overrides.yaml
retracts:
- s: domain:policy/house-refunds
p: governs
o: domain:product-line/home-cover
reason: Client A's contract replaces the house refund policy for home cover.It holds for readers in that unit and every unit below it. Three rules stop it from weakening the house model unnoticed:
- Only declared edges can be retracted, and a retraction needs a reason. A derived edge changes at its source. A retraction without a reason, or one naming an edge no layer above declares, is a lint error.
- A retraction is recorded. It is applied like everything else, so it arrives with an audit row and a digest (§8).
- A retraction is visible to the reader. The audit log never reaches a run, so
graph showlists the retraction, with its layer and reason, on the node it was taken from.
A department retracting an edge reaches every unit below it, which is further than a single project can reach. That is the same reach a department’s declarations have, and it gets the same visibility.
10. What the model is used for
Five uses, in the order they became available.
Direction: what the work is for
Every scoped run is told which unit it is working in, what that unit and each one above it is for, and which objective its own work advances. A department’s knowledge is written once and read by every unit below it. CONCEPTS already warns against keeping a copy in each.
The same data answers the question in reverse:
$ milieu graph trace objective:global/first-contact
objective:global/first-contact Resolve most contacts at the first touch
└─ advanced_by objective:customer-ops/one-touch-80 project:customer-ops
└─ advanced_by objective:acme/one-hour-credit project:acme
members mira
objective:global/retention Keep customers past their first renewal
└─ advanced by nothingAn organisational objective nothing below it advances is either finished or unowned, and either is useful to know without reading every charter.
Governance: reachability as a query
Closed-world, over what the view declares, with no inference and no retrieval changes.
$ milieu graph reach agent:mira
domain:refund accountable_for (declared)
├─ documented_by knowledge:global/articles/refund-policy
├─ documented_by knowledge:project/acme/articles/sla [project: acme]
└─ resolved_via — no route
domain:claim via skill:mira/respond (acts_on)
└─ resolved_via route:claims-api ✗ not granted to miraTwo findings in one output that otherwise require reading four files: Mira is accountable for refunds and nothing in the installation resolves a refund to a system, and her skill acts on claims through a route she does not hold.
The same query, started from a party, shows that the agent who owns the ParcelCo relationship cannot reach ParcelCo:
$ milieu graph reach domain:supplier/parcelco
domain:supplier/parcelco ParcelCo
├─ documented_by knowledge:global/articles/parcelco-contract
├─ owned_by agent:arun
├─ scoped_by project:customer-ops
└─ exposed_by route:parcel-tracking granted to mira ✗ not granted to arunThe same traversal answers the currency question the supersedes key was
introduced for, one step further out: which live documents still cite a
superseded concept. That is a join of documents against supersedes, and
without the graph it means reading every document.
Explainability: what a reply rested on
$ milieu graph why run:8231
domain:refund
├─ knowledge:global/articles/refund-policy read at 14:02 (human-reviewed)
├─ memory:agent/mira/refund-window recalled (agent's own note)
└─ route:claims-api → domain:claim/4471 called at 14:03The log already holds every line of this. The graph adds the first column: the run touched three sources, and all three are about refunds. The four-hour threshold in the reply came from an agent’s own note and from no policy article, and that is visible without reading the transcript.
Retrieval: a neighbourhood instead of a ranked list
Search fuses keyword and vector results. The graph adds a step before and a step after.
Before: the query is linked to domain nodes through class labels, aliases and
terms. “The customer wants a credit on their home cover” resolves to
domain:refund and domain:product-line/home-cover, and “has the courier collected
it” resolves to domain:supplier/parcelco.
After: the objects that say they are about each seed are added, then those
about the classes one subclass_of step from it, narrower or broader. A
document about credit notes is reachable from a query about refunds through
credit-note subclass_of refund, whether or not the two share a word. Reached
objects follow the text matches in the results, never displace them, and are
held to the search’s filters like any other hit.
Each search follows its own attachment. A knowledge search follows
documents, a memory search notes, so a note about a node is reached on
the same terms a document is. A knowledge search also names the route that
exposes a party the query names: a route is not a document and cannot be
opened, so it is named last and as itself, and only to an agent that holds the
gateway, because naming one it may not call is noise. The same fusion runs in
milieu knowledge search, so when you score the switch, you see what a run
would have got.
Two constraints apply. What is reached is budgeted, one budget per attachment,
because context is finite and a hub class like customer touches everything.
Anything reached only through the model is labelled as such, with the node it is
attached to, how it is attached, and the name the query used, as a stale
document is labelled, so the model can tell a direct hit from a neighbour.
Retrieval is on unless an installation turns it off
(MILIEU_GRAPH_RETRIEVAL=off). The default runs the other way from the
intuition about it: a larger search result reads like an expense, and it is
cheaper than what it saves, because a search that misses costs a whole further
model turn and a search that hits costs some extra tokens in one. An
installation whose corpus is one whose own words already find what its readers
mean buys nothing by it, and pays a larger context assembled on every run, on a
path with no quota at the end of it. The switch exists for that case. To decide
which kind of corpus you have, use
examples/domain-retrieval with
milieu eval compare.
Coordination: routing by subject
A message names what it is about, and accountable_for decides where it goes:
$ echo "how long is the window?" | milieu message --as arun send --about refund "refund window"
queued message 1 -> mira (accountable for domain:refund)message send --about and ask --about take a node where they took a
recipient. A question about pricing goes to whoever owns pricing, a question
about ParcelCo to whoever owns that relationship, and an installation that
reorganises changes one declaration. No skill body has to name a colleague.
Resolution is exact. A subject nobody declares they answer for is refused, and the refusal names who owns what it is a kind of, so the sender knows which declaration to add:
$ milieu message --as arun send --about credit-note "q"
error: no agent is accountable for domain:credit-note (credit-note); mira owns
domain:refund, which it is a kind of. Subjects resolve exactly: declare
domain:credit-note under [ontology] owns, or address the agent directlyWalking up to the owner of a broader class was the alternative, and it is the wrong default: routing a question about one supplier to whoever owns suppliers in general is a guess the sender never made and cannot see. Two owners is refused for the same reason (picking between two declarations is the operator’s decision, not the router’s), and so is addressing oneself.
The graph reaches other installations the same way. An agent card advertises
what it does in prose. It now also carries accountableFor, the identifiers a
peer’s router can act on. The installation’s own card, at
/a2a/.well-known/agent-card.json, lists the subjects it answers for and names
no agent at all: which agent holds a subject is the installation’s to change,
and addressing by subject keeps that choice with the installation. A peer posts to
/a2a/by-subject/<node>, which resolves the same way.
Addressing by subject doesn’t widen a credential. A key says “acts as mira”, and resolving a subject must not turn it into a key for whoever owns that subject, so the credential still has to be the resolved agent’s. It adds a check: the peer says what the exchange is about, and the installation confirms its credential belongs to whoever answers for it. A peer that has not noticed a reorganisation is refused. It doesn’t go on conversing with an agent that no longer owns the subject. A credential scoped to a subject instead of to an agent would be a new kind of key, and a new trust statement to go with it.
11. Storage and query
Everything a file declares is read from files. Every declared node and edge
lives in an ontology module, and every derived one in a project.toml or a
charter.yaml.
A view is built from those files on every read, along the scope chain, and
nothing is indexed. Queries are a breadth-first walk over the view with a depth
bound. At the size of a domain model a build takes milliseconds, and a graph
read from its source cannot be stale: a reorganisation, a new alias, or a
changed charter is in the next read, with no reindex to forget.
A store, for the edges no file holds
Attachment edges are read from knowledge, memory, and skill frontmatter on every view, which at the size of an installation’s corpus has stayed cheap enough. Inferred edges come from a model and have no file at all, so they need a database, one per layer, beside the search index it complements:
$MILIEU_HOME/var/global/graph.db
$MILIEU_HOME/var/projects/<code>/graph.db
$MILIEU_HOME/var/agents/<code>/graph.dbCREATE TABLE node (
uri TEXT PRIMARY KEY, -- domain:claim, project:acme, objective:acme/…, agent:mira
kind TEXT NOT NULL, -- class | instance | term | unit | objective | attachment
class TEXT, -- instances: the class uri
label TEXT,
aliases TEXT, -- JSON array, lower-cased, for entity linking
origin TEXT NOT NULL, -- declared | derived | inferred
trust TEXT NOT NULL, -- unverified | machine-confirmed | human-reviewed
source TEXT, -- the file, route or record it came from
digest TEXT, -- of the definition; changes when the meaning does
changed TEXT, -- RFC3339: when that digest last changed
attrs TEXT -- JSON: description, keyed_by, resolved_via, until
);
CREATE TABLE edge (
s TEXT NOT NULL,
p TEXT NOT NULL,
o TEXT NOT NULL,
origin TEXT NOT NULL,
trust TEXT NOT NULL,
since TEXT, -- RFC3339
until TEXT, -- inferred edges and objectives expire; NULL is indefinite
evidence TEXT, -- file path, audit seq, or model + prompt digest
PRIMARY KEY (s, p, o)
) WITHOUT ROWID;
CREATE INDEX edge_o ON edge(o, p); -- inverse traversalA unit’s database holds that unit’s own declarations and nothing it inherits, so
a reorganisation rewrites none of them: it changes which ones a read opens.
graph reindex writes them, one layer at a time, from the view scoped to that
layer: a view scoped further in has already shadowed the layer’s declarations
with the nearer ones, and the shadowed version is not in it to be written.
The files remain the source of truth, and a database is a projection of them: run reindex again to catch up, and a view is still built from files so that a changed module is in the next read with no reindex to forget. Only the database holds inference, which no file would rebuild. A rewrite therefore replaces a layer’s declarations and leaves its inferred edges alone, and a read drops an inferred edge wherever a file declares the same triple. This is the declared-wins rule of §8 enforced on read as well as on write, because a view may be built from layers whose databases were written at different times.
The proposal queue of §8 is not in this database. It
is append-structured, host-written, reviewed, and then thrown away, and putting
it in graph.db would make “is this in the graph” a question about a column
value. It lives under var/ontology/proposals/ and no read path joins it.
With a store, traversal is a recursive common table expression (CTE) with the same depth bound. The graph is the size of a domain model, thousands of nodes, so this takes microseconds. The depth bound is there to keep results readable.
A graph database was considered and not used. Milieu installs as one binary under one prefix, with embedded SQLite already carrying the full-text search (FTS) index, the vector index, the API keys, and the message queues. A graph database would be the first service Milieu requires, and it would add nothing at this scale. The reasoning that put the search index in SQLite applies unchanged.
Resource Description Framework (RDF) and OWL were considered too. The model is triple-shaped and exports to Turtle, JSON-LD, and Simple Knowledge Organization System (SKOS), because a domain model an organisation cannot take elsewhere is a lock-in nobody agreed to. Adopting the stack is a different question, and the answer is no for one reason: OWL is open-world, and the governance queries in §10 are closed-world. “No route resolves refunds” has to mean that there is no such route. Under open-world semantics it would mean only that none has been stated. A reasoner whose correct answer to “can Mira reach the payments API” is “unknown” is the wrong tool for the question that matters most here. The useful inference is transitive closure over a handful of predicates, which the recursive CTE already does, with an answer you can act on.
12. Surfaces
CLI, the organisation. It needs no graph:
| Command | Answers |
|---|---|
project create <code> --parent <code> --level <label> | a unit under another, with its level |
project tree | the units, their levels and parents |
project show <code> | a unit, what its members read, its roster, and the overseers that reach it |
project set <code> --parent <code> | a reorganisation, printing every chain it changes |
project overseer add|rm <code> <agent> | oversight granted or withdrawn |
charter show [--in <code>] | the full text a run scoped there is told, and its context_digest |
CLI, the graph. The milieu graph command group, read-heavy:
| Command | Answers |
|---|---|
graph classes [--in <code>] | what the domain model contains |
graph show <uri> | one node, its attributes, its edges both ways |
graph path <a> <b> | how two things are connected |
graph trace <objective|project> | what an objective is advanced by, or what a unit’s work advances |
graph why <run:seq> | what a run rested on |
graph history <uri> | the definitions a node has had, and when and by whom each was applied |
graph reach <uri> | closed-world reachability over ownership, skills, routes, and parties |
graph orphans | classes nothing documents, documents about nothing, objectives nothing advances, and dangling edges |
graph proposals [--sort count|recent] [--all] | what agents have reached for and the model does not have |
graph propose accept <id> --file <module> [--write] | the change accepting a proposal makes to a module file, as a diff. --write makes it |
graph propose reject <id> --reason … | decline it, and stop it resurfacing |
graph propose reset <id> | forget a proposal, so it may be proposed afresh |
graph export --format turtle|jsonld|dot | the model, portable |
There is no graph neighbors command: neighbors is a verb of the agents'
graph tool only. graph reindex writes each layer’s declarations into its
own database (§11). A view is still built from the
files, so a changed module appears in the next read without a reindex.
Lint. milieu lint checks two groups. For the organisation: a parent that does
not exist, a cycle, a chain past the depth cap, an advances naming anything
but an objective above, an oversees naming a unit that does not exist, a
lapsed objective, an objective advancing one whose statement changed after it
was last stated, and a chain whose charter, terms, and department context
compose past the byte cap. For the graph, the checks that catch the ways a graph
goes wrong: an edge whose ends violate the declared subject and object classes,
an instance of an undeclared class, a resolved_via or ontology-instance
naming a route or instance that does not exist, an about: or acts-on: naming
a node nothing declares or the wrong kind of node, a document about: a node
whose definition changed since the document was last verified, a retraction
without a reason or naming an edge no layer above declares, and related_to
more than a share of the declared edges. A class nothing documents is not a lint
finding. graph orphans lists it instead.
The built-in graph tool. It is permission-gated in the existing syntax and
available to a skill only where both the skill declares it and the agent holds
it, walking the view the run’s scope reads. It has five read verbs, classes,
find, show, neighbors, and path, and one write-adjacent verb, propose.
find is a forgiving lookup, from a name someone has in mind to the nodes it
could mean, matching within any node’s identifier, label, or aliases. Retrieval
links a query’s whole words to classes, instances, and terms only, so the two
can return different nodes, although they share what a node may be called. propose
writes to the review queue and never to the graph, under its own grant,
graph(propose:*). There is no graph(add:*), and the propose grant is
separate from the read grants so an installation can take the signal without
taking the queue, or the reverse. See §8.
Provisioning. Project has parent, level, and charter (a path), and
Agent has oversees. kind: Organisation, at most one per manifest, carries
the global charter and the organisation’s name. kind: Ontology provisions the
domain model. All three are reconciled by milieu apply, so the organisation
and its domain model are provisioned from the same file as everything else and
live in the same review. Taking one out of the manifest removes nothing, and
--prune does not touch them. A unit’s declared ontology modules do replace
the ones installed for it. The schema has the fields, and the editor support
built on it picks them up from there.
13. Worked example: the contact centre
Extending the installation in GETTING-STARTED: Mira answers customers, Arun dispatches, Vera audits, and ACME is a project whose SLA overrides the house refund threshold.
The organisation. Without units there is a flat support-desk project and a
flat acme project, and the escalation article that holds for both is copied
into each. With units:
global Example Broadband (organisation)
└─ customer-ops Customer operations (department) members: arun
├─ support-desk Support desk (team) members: mira, arun, vera
└─ acme ACME account (account) members: miraThe organisation’s charter is the one in §3. Customer
operations’ charter carries one-touch-80, and ACME’s adds one objective:
# etc/projects/acme/charter.yaml
mission: Keep ACME's sites inside their one-hour SLA.
objectives:
- id: one-hour-credit
statement: Credit an SLA breach within the hour.
advances: [customer-ops/one-touch-80]The escalation article moves to var/projects/customer-ops/knowledge/, once.
The domain. The house model, six classes and four relations:
# var/global/ontology/support.yaml
classes:
- { name: customer, keyed_by: customer_id, resolved_via: route:crm }
- { name: conversation, keyed_by: conversation_id }
- { name: refund, aliases: [credit, goodwill payment] }
- { name: credit-note, subclass_of: refund }
- { name: sla-tier }
- { name: outage }
relations:
- { name: raised_by, from: conversation, to: customer }
- { name: resolved_by, from: conversation, to: refund }
- { name: entitled_to, from: customer, to: sla-tier }
- { name: affects, from: outage, to: customer }
instances:
- { class: sla-tier, key: standard, label: Standard }
- { class: sla-tier, key: gold, label: Gold }Customer operations deals with the courier, so the supplier and its term live in the department’s layer:
# var/projects/customer-ops/ontology/parties.yaml
classes:
- { name: supplier }
instances:
- { class: supplier, key: parcelco, label: ParcelCo }
terms:
- { term: the courier, means: domain:supplier/parcelco }
- { term: truck roll, definition: Sending an engineer to the customer's premises. }ACME’s layer adds one instance and one alias:
# var/projects/acme/ontology/acme.yaml
instances:
- { class: customer, key: acme, label: ACME Corp, entitled_to: sla-tier/gold }
classes:
- { name: refund, aliases: [service credit] } # shadows the inherited aliases here onlyThe attachments are declared where each object lives:
- The global refund policy carries
about: [domain:refund]. - ACME’s SLA article carries the same and lands in ACME’s layer.
- Mira’s
respondskill declaresacts-on: [domain:conversation]. - The customer relationship management (CRM) route declares
ontology-class: customer. - The
parcel-trackingroute declaresontology-instance: supplier/parcelco. - Arun’s
agent.tomlownsdomain:supplier/parcelco. project:acmescopesdomain:customer/acme.
What changes:
- A run
--in acmeis told it works for the ACME account inside customer operations, and that crediting a breach within the hour advances the department’s first-touch objective and, through it, the organisation’s. The prompt now says why the one-hour threshold matters as well as what it is. - The escalation article is written once, in customer operations, and read by both teams. Editing it is one change, and there is no second copy to drift.
- Vera, a member of the support desk, reads the department’s layer but not
ACME’s by membership. Her
agent.tomlgrantsoversees = ["customer-ops"], so she can run--in acmeto audit the account, read-only, without joining its roster, andproject show acmelists her as an overseer, so the grant is as visible as the roster it bypasses. - Every run in the support desk and in ACME opens with customer operations' department context, so the regulator escalation rule is written once and cannot be missing from one team’s instructions.
- With retrieval through the model turned on, a customer who asks for a
“service credit” while the conversation runs
--in acmereaches the refund policy and ACME’s override, because the ACME alias links the phrase anddocumentspulls both articles. Without the graph, the phrase has to appear in one of the documents. - “Has the courier collected my router” links to
domain:supplier/parcelco, which isexposed_by route:parcel-tracking, which Mira holds.graph reachon the same node reports that Arun owns the ParcelCo relationship and holds no grant on its route. graph whyon the reply distinguishes the four-hour house threshold from ACME’s one-hour override, and says which one the run read.graph orphansreportsdomain:outagedocumented by a news item that went stale in March,domain:credit-notedocumented by nothing at all, and a globalretentionobjective that no unit’s objective advances.
14. An installation that declares nothing
An installation that declares no parents, no charters, and no ontology is
unchanged in every respect. Every scope chain is [agent, project, global], the
system prompt is byte-identical to what it was before units existed (so a
provider’s prefix cache sees no change either), milieu graph reports an empty
model, the built-in tool answers from one, lint has nothing to check, and
knowledge search fuses keyword and vector results as it always has.
The two halves are independent. Units and charters need no graph: an installation can have a department tree and a mission statement in every prompt and never declare a class. A graph needs no units: a flat set of projects gets every domain capability in this document.
This is the posture embeddings already take: a capability that improves retrieval when configured and is absent when not. An organisation gets value from Milieu without writing a class, and the first useful ontology is six classes long. The usual failure of an enterprise ontology is a large model nobody maintains, and the defence against it is that the small version has to be useful on its own. The same holds for charters: one mission line at the organisation and one objective per team is a legal and useful charter.
15. Open design questions
Five remain open.
Where about: lives. It is in frontmatter, which keeps the claim beside
the prose that justifies it and diffs in the same review. A sidecar file would
leave an existing Markdown tree untouched, which matters because “your existing
pile of markdown works on day one” is a promise the knowledge layer already
makes. The two are not exclusive, but supporting both doubles the places to look.
Only the frontmatter form exists.
How far reference instances extend. region/emea is clearly reference data and
claim/4471 is clearly not. customer/acme sits between them, and its position
moves per installation: an insurer has millions of customers, a
business-to-business (B2B) support desk has forty. Parties are the same question
from the other side: a retailer has three logistics suppliers and four thousand
product suppliers. A rule that works for both may not exist, in which case the
answer is a cap and a lint warning.
What a bad entity link costs. A wrong link changes what a run reads without telling anyone: a stale document is labelled, and a mis-linked one is not. Retrieval labels every traversal-reached document with the node it is about and the name the query used, and keeps it after the text matches. The retrieval suite tests whether that is enough, and retrieval was held back until that suite had been scored.
Whether touched should ever be materialised.
§7 argues for computing it from
the log. The counter-argument is a real question the join answers slowly at
scale: “which parts of the domain has this installation never touched in six
months” scans the whole log. A materialised rollup with a retention window may
be the answer, and it is a different object from an edge.
What earns machine-confirmed. The ladder is borrowed and the middle rung is
not yet defined for edges. The position taken here is that machine-confirmed
means a machine checked it, as the knowledge layer already implies: there,
verified is an event with a named actor. Under that reading a model’s output
is never machine-confirmed, however confident, and the rung is earned only by a
check that could have failed: an edge that type-checks against declared domain
and range with both ends declared; a resolves edge confirmed by calling the
route and getting the declared key back; an edge an eval case fails without.
The candidate to reject is use without complaint. An edge that has been on the
retrieval path for fifty runs and drawn no correction has not been checked, and
trusting it would create a loop where an edge gains standing by having been
used. Settling this decides how much of the graph can ever be trusted without a
person, so it stays open.