# ONTOLOGY

ATLAS runs an **operational** ontology: object types, link types, action types and functions are
stored rows that the server validates against at request time. It is deliberately not RDF/OWL —
there is no triple store and no reasoner. W3C PROV-O informs the evidence model only
(`docs/RESEARCH.md` records the source).

Both segment packs are at `ontologyVersion` **1.0.0** on a clean seed. The version is stored per
workspace *and* copied onto every entity, so a record always states which schema it was written
under. Publishing a new type from Ontology Studio bumps the workspace version and journals
`ontology.published`, which is what invalidates any open scene.

Definitions live in `packages/ontology/src/defense.ts` and `commercial.ts`, are validated and
default-filled by `packages/ontology/src/index.ts`, loaded into a per-workspace registry by
`apps/api/src/registry.ts`, and re-read after any ontology publish.

## Identity rules

- Every object, link, evidence row and event carries a `uid` string that is stable for the life of
  the record. SurrealDB record ids exist but are incidental — nothing matches on them.
- Unique indexes enforce it: `entity_uid ON entity FIELDS uid UNIQUE`,
  `otype_uid ON ontology_type FIELDS workspaceId, uid UNIQUE`, plus the equivalent per table.
  (`ASSERT UNIQUE` on a field misbehaves on SurrealDB 3.2.3 — it rejects fresh values — so
  uniqueness is expressed as a unique index, which was measured to behave.)
- Entity uids must be at least 3 characters (`ASSERT string::len($value) >= 3`); seed data uses
  readable namespaced uids such as `def-vessel-03`, `com-wh-tuas`, `link-reserve-def-vessel-03`.
- Ontology type uids created through Ontology Studio must match
  `/^[A-Za-z][A-Za-z0-9_]{1,60}$/` (`apps/api/src/ontology-studio.ts`). Dots are rejected — the
  Studio's own default suggestion `ops.review_ticket` therefore 400s (known defect, see
  `docs/BUILD_STATUS.md`).

## Storage envelope vs. flexible payload

`entity` is `SCHEMAFULL`, which fixes the *envelope* every record must expose, while `properties`
is a FLEXIBLE object whose shape comes from the stored type definition:

| Field | Meaning / constraint |
|---|---|
| `uid`, `workspaceId`, `segment`, `objectType` | identity and scope; `segment INSIDE ['defense','commercial']` |
| `displayName`, `status` | human label (len ≥ 1) and lifecycle status |
| `version` | optimistic concurrency; every committed write bumps it (`ASSERT $value >= 1`) |
| `properties` | FLEXIBLE object, validated server-side against the type's property definitions |
| `trustClass` | `observed` \| `derived` \| `simulated` \| `ai_proposed` — how we know it |
| `dataQuality` | `fresh` \| `stale` \| `conflicting` \| `incomplete` \| `invalid` — how far we trust it |
| `synthetic` | true for demonstration data, false when a human or import asserted it |
| `evidenceRefs`, `observationIds` | pointers into `evidence` / `observation`; the join the Evidence screen and lineage walk |
| `spatial` | optional `{x, y, siteUid}` used by the Scene view |
| `createdAt`, `updatedAt`, `observedAt`, `scenarioTime` | real time vs. in-scenario time, kept separate |

Two independent axes matter: `trustClass` says where a value came from, `dataQuality` says whether
sources agree. An AI proposal is `ai_proposed`, never silently `observed`, and a disputed figure
becomes `conflicting` rather than overwriting the earlier value.

## Segment packs (measured on a clean seed)

**Defense — `workspace-defense`**, 21 object types, 22 link types, 5 action types, 4 functions.
Object types: Organization, Unit, Team, PersonnelRole, Vessel, Asset, Vehicle, Sensor, Depot, Site,
Route, SupplyItem, InventoryLot, Shipment, ResponseMission, MaintenanceWorkOrder, Incident,
Observation, Capability, Decision, Evidence.

**Commercial — `workspace-commercial`**, 21 object types, 24 link types, 7 action types,
4 functions. Object types: Organization, Supplier, Customer, Warehouse, Plant, Asset, Component,
Product, InventoryLot, InventoryTransfer, Order, OrderLine, Shipment, Route, Contract, Risk,
MaintenanceWorkOrder, Observation, Decision, Evidence.

Types marked `segment: 'shared'` (Organization, Evidence, Observation, Decision, InventoryLot,
Route, Asset, MaintenanceWorkOrder) appear in both packs so the two worlds stay comparable.

### Link rules that actually bite

`entity_link` is `TYPE RELATION FROM entity TO entity`, so an edge is a row with its own `uid`,
`version`, `properties` and journal entry. The stored `ontology_link` definition then decides which
endpoints are legal, and the API enforces it before writing:

| Link | From → To | Note |
|---|---|---|
| `ALLOCATED_TO` | Vessel → ResponseMission | defense; the readiness story's allocation edge |
| `ASSIGNED_TO` | Team → ResponseMission | crew assignment |
| `OPERATES` | Unit → Vessel \| Asset \| Vehicle | multi-target union |
| `MAINTAINS` | MaintenanceWorkOrder → Vessel \| Asset \| Vehicle | defense / shared |
| `EVIDENCES` | Evidence → Incident \| Observation \| MaintenanceWorkOrder \| Vessel | provenance edge, union target |
| `LOCATED_AT` | Vessel \| Vehicle \| Asset \| Unit \| Depot → Site | position without coordinates |
| `AFFECTS` | Risk → Supplier \| Shipment \| Order \| Warehouse \| Component \| Asset | commercial only |
| `SUPPLIES` | Supplier → Component | `uniquePair: false`, so repeat supply facts coexist |
| `REQUIRES_COMPONENT` | Product → Component | carries `quantityPerAssembly`, `unit` (bill of materials) |
| `HOLDS` / `OF_ITEM` / `DESTINES_TO` | Warehouse → InventoryLot → Component\|Product; Transfer → Warehouse | the inventory ledger chain |
| `SHIPMENT_OF` / `ORIGINATES_FROM` / `ARRIVES_AT` | Shipment → Component\|Product; → Supplier\|Warehouse; → Warehouse | planned-supply chain |
| `SUPPORTED_BY` | Decision → Evidence | decision provenance |

A violating write is refused with per-field detail naming both endpoints, e.g.
`affects cannot start from Warehouse` / `endpoint_type_mismatch`. That rejection is exercised by
journey 16 and appears in devtools as a deliberate `400`, not an accident.

### Action types (governed writes, not free-form edits)

Defense: `update_object_status` (operator), `record_maintenance_failure` (operator),
`activate_reserve_asset` (operator), `link_objects` (analyst), `validate_observation` (analyst).
Commercial: `update_object_status`, `record_supplier_disruption`, `create_inventory_transfer`,
`create_expedited_order`, `ingest_arrival`, `link_objects`, `validate_observation`.

Each declares `eligibleObjectTypes`, typed `inputs` (including `record` inputs that must resolve to
a row in the same workspace), and a `requiredRole`. Segment guards are enforced: a Defense action
asked of the Commercial workspace is refused rather than silently misapplied. Only five handler
implementations exist (`apps/api/src/actions.ts` `registeredImplementations()`), so a name in the
registry is not enough — the code must be registered too.

## Functions (derived, never stored as truth)

Defense: `defense.readiness`, `defense.missionAllocation`, `defense.reserveEligibility`,
`defense.impactTraversal`. Commercial: `commercial.shortage`, `commercial.allocation`,
`commercial.interventionCost`, `commercial.dependencyTraversal`.

They are pure over stored rows (`packages/ontology/src/metrics.ts`) and are what the metric tiles,
action preconditions and copilot answers quote. Nothing caches their output as a field, so a tile
can never disagree with the ledger it recomputes from.

## Validation boundaries (where a write is refused)

1. Contract level — zod schemas in `packages/contracts` reject malformed payloads before any query.
2. Ontology level — unknown property, wrong data type, out-of-range number, missing required
   property, or a status outside the type's enum are refused by `validateProperties`.
3. Link level — endpoint types checked against the stored `ontology_link`; wrong direction or type
   returns `endpoint_type_mismatch` for each offending end.
4. Ledger level — `reservedQuantity` must equal the sum of `properties.reservations`; both seed
   generators and validation enforce it, so "usable" arithmetic cannot quietly drift.
5. Concurrency level — an action preview carries `baseVersions`; if a target moved, the commit
   fails with a version conflict instead of overwriting a newer truth.
6. Scope level — every read and write is filtered by `workspaceId` *and* `segment`; cross-workspace
   reads, actions, evidence and SSE delivery are refused (integration test 11).

## Editing the ontology at runtime

Ontology Studio (`/api/ontology/types`, `/api/ontology/links`) creates a type, an instance and a
valid link, then republishes. A created type is stored in `ontology_type` with `source: 'user'`,
appears in the schema lens, survives a restart and a backup/restore cycle, and its instances are
validated exactly like seeded ones. Journey 16 creates `e2e_inspection_*` types this way and then
proves an invalid link is rejected.

## Evidence model

PROV-O-flavoured and deliberately boring: `evidence` rows carry `kind`
(`source_record` \| `import_file` \| `action_receipt` \| `model_output` \| `manual_note`), a
server-computed `sha256` (never browser-supplied), byte count, `artifactPath` under
`data/evidence/`, and the optional `originalRow`. Files are written by the server at import time,
so an evidence row can be re-hashed against disk to prove the artifact behind a number.
