Skip to content

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

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 asMeaning
Knowledgedocumentsthis article is about that domain thing
Memorynotesthis agent’s note is about that domain thing
Skills / toolsacts_onthis capability operates on that class
Gateway routesresolveslive instances of that class live behind this route
Gateway routesexposesthis route is how that outside party is reached
Agentsaccountable_forthis agent owns that part of the domain
Projectsscopesthis workspace is about that part of the domain
Runstouchedthis 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-b

Units: 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.

A run in acme reads the agent mira, then acme, then its parent customer-ops, then global. The sibling unit support-desk shares the parent but is not in the chain.

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 acme requires mira on 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.md came 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-31

Four fields, all optional:

FieldMeaning
visionwhere the unit is going (usually only the organisation states one)
missionwhat the unit is for, in one sentence
objectives[].id, statementwhat the unit is trying to achieve now
objectives[].advancesthe objectives, in an ancestor or global, this one serves
objectives[].untilwhen 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.

The system prompt's head layers in order: the agent's identity brief, the charter and unit terms, the recall index, the memory index, the department context, the unit's own instructions, and the skill body. The charter and department context are the layers units and charters add.

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-contact

Five rules keep it small:

  1. Vision and mission for every unit in the chain, outermost first, one line each.
  2. Objectives for the run’s own unit only, each followed by its advances path. 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.
  3. Lapsed objectives are left out and reported by lint, as a stale news item is labelled stale.
  4. 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.
  5. 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 a keyed_by and a resolved_via route, 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.

Domain classes and reference instances joined by solid domain edges such as settles and filed_against, with knowledge, memory, agent, skill and route nodes attached to refund and claim by dashed edges.

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.

PredicateSubject → objectTransitiveNotes
subclass_ofclass → classyescredit-note subclass_of refund
instance_ofinstance → classnoreference instances only
part_ofnode → nodeyesmereology: region/emea part_of region/global, and also unit → parent unit
same_asnode → nodesymmetricreconciles the same thing named twice
supersedesnode → nodeyeslifts the existing Open Knowledge Format (OKF) key to the graph
related_tonode → nodenothe 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.

PredicateSubject → objectTransitiveDerived from
part_ofunit → unityesparent in project.toml (the structural predicate, reused)
pursuesunit → objectivenothe objectives in the unit’s charter.yaml
advancesobjective → objectiveyesadvances: 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: false

Because 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/parcelco

It 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.

FormExampleComes from
domain:<class>domain:claimdeclared
domain:<class>/<key>domain:region/emeadeclared (reference instances only)
term:<slug>term:truck-rolldeclared (definition-only terms, §6)
org:rootorg:rootthe organisation, labelled with the global charter’s name
project:<code>project:acmeproject code
objective:<unit>/<id>objective:customer-ops/one-touch-80a charter, with global for the organisation’s
knowledge:<layer>/<concept-id>knowledge:global/articles/refund-policyknowledge.ConceptID, layer-qualified
memory:<layer>/<name>memory:agent/mira/refund-windowmemory name and layer
agent:<code>agent:miraagent code
skill:<agent>/<name>skill:mira/respondskill key
route:<name>route:claims-apigateway route name
tool:<name>tool:knowledgebuilt-in or granted command
run:<seq>run:8231the 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.

PredicateSubject → objectDeclared whereOr derived from
documentsknowledge → domainabout: in OKF frontmatterNone
notesmemory → domainabout: in memory frontmatterNone
acts_onskill → domain classacts-on: in skill frontmatterNone
resolvesroute → domain classontology-class on the routeNone
exposesroute → domain instanceontology-instance on the routeNone
accountable_foragent → domainontology.owns in agent.tomlNone
scopesproject → domainontology.scope in project.tomlNone
touchedrun → domainNonethe 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.

OriginWritten byRebuildableExample
declareda person, in a reviewed filenoclaim filed_against policy
derivedMilieu, from state it already holdsyesagent:mira accountable_for domain:refund, read out of agent.toml, and project:acme part_of project:customer-ops, read out of project.toml
inferreda model, from the corpusyes, and expiresdomain: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:

  1. 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.
  2. Inferred edges expire. Each carries an until (two weeks by default), and graph sweep removes 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.
  3. 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:

  1. 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:8231 resolves 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.
  2. Changed since verified. A knowledge document about: a node whose definition changed after the document’s newest verified entry 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 that advances it. 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.
  3. 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:

FieldWhy it is there
subject, predicate, objectthe claim
rationaleseach distinct reason given, in the agents’ own words
proposerseach proposing run’s agent, skill, and skill digest, and when
sourcesthe nodes the runs had already read when they proposed
count, first_seen, last_seenhow many distinct runs have proposed the same triple
status, reason, decided_by, decided_atthe 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.

A proposal starts open and counts each further run that proposes it. Accepting it with write makes it accepted. Rejecting it with a reason makes it rejected, which refuses later proposals of the same edge until a reset forgets it.

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 accept writes the triple into the layer’s declared ontology/ 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:acme alone.
  • 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:refund replaces 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 show lists 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 nothing

An 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 mira

Two 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 arun

The 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:03

The 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.

A query goes to keyword and vector search and, separately, is linked to domain nodes. Objects about the seed nodes and their subclass_of neighbours are added, budgeted and labelled, and follow the text matches in the results.

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 directly

Walking 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.db
CREATE 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 traversal

A 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:

CommandAnswers
project create <code> --parent <code> --level <label>a unit under another, with its level
project treethe 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:

CommandAnswers
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 orphansclasses 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|dotthe 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: mira

The 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 only

The 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 respond skill declares acts-on: [domain:conversation].
  • The customer relationship management (CRM) route declares ontology-class: customer.
  • The parcel-tracking route declares ontology-instance: supplier/parcelco.
  • Arun’s agent.toml owns domain:supplier/parcelco.
  • project:acme scopes domain:customer/acme.

What changes:

  • A run --in acme is 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.toml grants oversees = ["customer-ops"], so she can run --in acme to audit the account, read-only, without joining its roster, and project show acme lists 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 acme reaches the refund policy and ACME’s override, because the ACME alias links the phrase and documents pulls 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 is exposed_by route:parcel-tracking, which Mira holds. graph reach on the same node reports that Arun owns the ParcelCo relationship and holds no grant on its route.
  • graph why on the reply distinguishes the four-hour house threshold from ACME’s one-hour override, and says which one the run read.
  • graph orphans reports domain:outage documented by a news item that went stale in March, domain:credit-note documented by nothing at all, and a global retention objective 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.