Reference

Semantic graph

The JSON that appling graph prints: nodes, edges and module dependencies, with every field, node kind and edge kind.

The semantic graph is the model as data. Every element becomes a node identified by its qualified name, every reference becomes an edge, and module dependencies are computed from the edges. The IDE draws it, the AI tools query it, code generation extensions read it and releases store it. appling graph prints it as JSON; in code, buildGraph from @appling/model builds it (see API).

Example

This model has two modules, a cross module reference and an invariant:

module crm {
  export Account

  entity Account {
    name: String
  }
}

module sales {
  enum OrderStatus {
    draft
    placed
  }

  entity Order {
    customer: crm.Account
    status: OrderStatus state default draft
    total: Money

    invariant "total never negative" { total >= 0 }
  }
}
flowchart TB
  subgraph m0["crm"]
    n0["Account<br/><small>entity</small>"]:::domain
  end
  subgraph m1["sales"]
    n1["OrderStatus<br/><small>enum</small>"]:::domain
    n2["Order<br/><small>entity</small>"]:::domain
  end
  n2 -->|"type"| n0
  n2 -->|"type, uses"| n1
  classDef domain fill:#3d2510,stroke:#3d2510,color:#f7f2ea
  classDef behavior fill:#6b3f12,stroke:#6b3f12,color:#f7f2ea
  classDef application fill:#a85f17,stroke:#a85f17,color:#f7f2ea
  classDef interface fill:#d98a1c,stroke:#d98a1c,color:#0c0a08
  classDef experience fill:#f2b632,stroke:#f2b632,color:#0c0a08
  classDef cross fill:#1b1712,stroke:#7a6e5e,color:#f7f2ea
The semantic graph Appling builds from this modelL1 Domain

appling graph prints 12 nodes, 14 edges and one dependency. An excerpt:

{
  "nodes": [
    {
      "qn": "sales",
      "kind": "Module",
      "layer": "cross",
      "level": "out",
      "module": "",
      "props": {},
      "data": {}
    },
    {
      "qn": "sales.Order",
      "kind": "Entity",
      "layer": "domain",
      "level": "medium",
      "module": "sales",
      "props": {},
      "data": {}
    },
    {
      "qn": "sales.Order.status",
      "kind": "Field",
      "layer": "domain",
      "level": "in",
      "module": "sales",
      "props": {
        "type": "OrderStatus",
        "modifiers": ["state"],
        "default": "draft"
      },
      "data": {
        "type": { "$type": "TypeRef", "decl": "sales.OrderStatus" },
        "modifiers": ["state"],
        "default": {
          "$type": "NameExpr",
          "name": "draft",
          "type": "OrderStatus",
          "target": "sales.OrderStatus.draft"
        }
      }
    },
    {
      "qn": "sales.Order.invariant[\"total never negative\"]",
      "kind": "Invariant",
      "layer": "domain",
      "level": "in",
      "module": "sales",
      "props": {
        "label": "total never negative",
        "condition": "total >= 0"
      },
      "data": {
        "label": "total never negative",
        "condition": {
          "$type": "BinaryExpr",
          "left": { "$type": "NameExpr", "name": "total", "type": "Money", "target": "sales.Order.total" },
          "op": ">=",
          "right": { "$type": "NumberExpr", "value": 0, "type": "Int" },
          "type": "Bool"
        }
      }
    }
  ],
  "edges": [
    { "from": "sales", "to": "sales.Order", "kind": "contains" },
    { "from": "sales.Order", "to": "sales.Order.status", "kind": "contains" },
    { "from": "sales.Order.customer", "to": "crm.Account", "kind": "type" },
    { "from": "sales.Order.status", "to": "sales.OrderStatus", "kind": "type" },
    { "from": "sales.Order.status", "to": "sales.OrderStatus.draft", "kind": "uses" },
    { "from": "sales.Order.invariant[\"total never negative\"]", "to": "sales.Order.total", "kind": "uses" }
  ],
  "dependencies": [
    { "from": "sales", "to": "crm", "count": 1 }
  ]
}

Top level

KeyContent
nodesEvery element, in document order (files in the order they were loaded, each element before its members)
edgesFor each element in node order its contains edges and references, then all uses edges; each edge appears once
dependenciesModule to module dependencies. Only appling graph adds this key; releases and buildGraph hold nodes and edges only

Nodes

FieldMeaning
qnThe qualified name: the module path, then the names down to the element, such as sales.Order.status. Nested modules give a.b.X
kindThe AST type, such as Entity, Field or Transition
layercross, domain, behavior, application, interface or experience
levelThe zoom level the element belongs to: out, medium or in
moduleThe qualified name of the module that owns the element; for a module, its parent module, or "" at the top
propsProperties for display
dataThe same properties for tools

Layer

A module is cross. A top level declaration gets the layer of its keyword:

LayerKinds
crossModule, Role, Actor, Tenant, Migration
domainEnum, ValueType, Entity
behaviorEvent, Action, Machine, Job
applicationQuery, Projection, External, UseCase
interfaceInterface
experiencePage, Navigation, Flow, Messages

A nested element takes the layer of the declaration that contains it: a field of an entity is domain, a parameter of an action is behavior.

Level

LevelNodes
outModules and use cases
mediumEvery other top level declaration
inNested elements, such as fields, enum values, transitions and endpoints

props and data

Both hold the element’s own properties, without its name and without the elements nested in it (those are nodes of their own, linked by contains). Properties that are empty, false or missing are left out.

propsdata
PurposeDisplay, such as the IDE inspector and the graph rowsTools, such as code generation extensions and AI
Scalars (label, modifiers, kind, verb, path)As they areAs they are
Nested parts that are not elements (types, expressions, preconditions, steps, sorts)Canonical source text, such as "total >= 0"Objects with $type and their own properties
ReferencesLeft out, they become edgesThe qualified name of the target, or null when it does not resolve
ExpressionsSource textExprData with the computed type and, for names, the target

So an action’s precondition shows as "requires { lines.size() > 0 } else EmptyOrder" in props and as { "$type": "Precondition", "condition": { ... }, "error": "sales.PlaceOrder.EmptyOrder" } in data.

Node kinds

Only some AST types become nodes. All other AST nodes fold into the props and data of the nearest element that contains them, and their references become edges of that element.

Named elements:

KindDeclared by
Modulemodule
Role, Actor, Tenantrole, actor, tenant
Enum, EnumLiteralenum and its values
ValueTypevalue
Entity, Fieldentity and the fields of entities, value types, events and actors
Eventevent
Action, Param, ErrorDeclaction, its inputs (and the parameters of use cases and external operations), and declared errors
Machine, Jobmachine, job
Queryquery
Projection, ProjectionFieldprojection and its fields
External, ExternalOperationexternal and its op
UseCaseusecase
Interfaceinterface
Page, Navigation, Flow, Messagespage, navigation, flow, messages
Migrationmigration

Unnamed elements, which get a key instead of a name:

KindKeyExample
Invariantinvariant[<label as JSON string>]sales.Order.invariant["total never negative"]
Policypolicy[<operations> fields <fields>], each part only when presentsales.Order.policy[read fields total], sales.Order.policy[create,update,delete], sales.PlaceOrder.policy[]
Transitiontransition[<from states>-><to state>]sales.OrderFlow.transition[draft,placed->cancelled]
Endpointendpoint[<verb> <path>]sales.PublicApi.endpoint[GET /orders]
SetEffecteffect[set <field>]sales.PlaceOrder.effect[set placed_at]
CreateEffecteffect[create <entity>]sales.PlaceOrder.effect[create Invoice]
EmitEffecteffect[emit <event>]sales.OrderFlow.transition[draft->placed].effect[emit OrderPlaced]

Lists keep their separators without spaces, and references in keys are written as in the source. When siblings share a key, the second gets #2, the third #3, and so on: policy[read]#2.

Everything else folds into its element, for example types (TypeRef), expressions, preconditions, assignments, sorts, use case steps and handlers, page components (list, detail, form), navigation items, messages and migration steps.

Edges

KindMeaning
containsFrom an element to each element directly inside it, looking through parts that are not elements
usesFrom an element to what a name in one of its expressions stands for: a field, an enum value, a parameter, a role and so on
any otherA reference, named after the referencing property

The reference kinds, with the element the edge starts from:

KindFromTo
typeA field, a parameter, or an external operation (its return type)The enum, value type or entity
entityAn action, query or tenant; a create effectThe entity
fieldA machine; a set effect; a query (its sort fields)The field
fieldsA field policyThe fields it covers
rolesAn actorIts roles
errorAn action (its preconditions); a use case (its error handlers)The error
eventA transition or job; an emit effect; a use case (its await steps)The event
actionA transition or jobThe action it runs
actionsA page (its list and detail components)The actions offered
targetA use case (its calls); an endpointThe action, query, use case or external operation
sourceA projectionIts entity or query
projectionAn endpoint; a page (its components)The projection
picksAn endpoint; a page (its components)The picked projection fields
queryA page (its list components)The query
submitsA page (its form components)The action or use case
pageA navigation (its items)The page
pagesA flowIts pages
usecaseA flowIts use case

References made by parts that are not elements (preconditions, sorts, steps, components, navigation items) belong to the element that contains them. An edge appears once per element, target and kind, however often the reference repeats.

Dependencies

dependencies aggregates every edge except contains whose ends lie in different modules:

FieldMeaning
fromThe module that references
toThe module it references
countThe number of edges behind the dependency

The IDE draws these at the Modules zoom level, labeled with the count.

ExprData

An expression inside data:

FieldMeaning
$typeBinaryExpr, UnaryExpr, NameExpr, MemberExpr, IndexExpr, ListExpr, StringExpr, NumberExpr, BoolExpr or NullExpr
typeThe computed type, written as in Appling: Bool, Money, OrderStatus, Line[]
targetFor names and members: the qualified name of the element it stands for, such as sales.Order.total
op, left, rightBinaryExpr: operator and operands
op, operandUnaryExpr: ! or - and its operand
name, call, argsNameExpr: the name; call and args for builtin calls such as duration("1h")
receiver, member, call, argsMemberExpr: lines.size() has the receiver lines, the member size and call: true
receiver, indexIndexExpr
itemsListExpr
valueStringExpr, NumberExpr, BoolExpr

See Expressions for the language itself.

TypeData

A type inside data, such as a field’s type:

FieldMeaning
$typeTypeRef
primitiveA built in type: String, Text, Int, Decimal, Bool, Date, DateTime, Duration, Money, File, Uuid
declInstead of primitive: the qualified name of an enum, value type or entity
manytrue for a list, Line[]
optionaltrue for an optional type, DateTime?