Metamodel specification · verbatim from DATA-OPERATION-METAMODEL.md · raw Markdown

Data & Operation model metamodel

Specy Data & Operation Metamodel Data & Operation Metamodel 1 1..n 0..n 0..n 0..n 0..n 0..n 1..n 1 0..n 1 0..n 0..n 0..n 0..1 0..1 0..n 0..1 0..n 0..1 0..1 Model strict | observed; implements a bounded context or organization Module derived dependencies; implements a domain module External declaration external module | external struct — named, never described Trait bundle Named · Module · Type · WithChildren · ChildOf · AttachedTo · Structural · Typed · WithValue · Invocable · WithStates · WithContract · WithFlow Conformance level strict (designed) · observed (extracted) Global binding typed, mutable, one initial value; a function-typed global holds an operation Operation signature · depends on · reads · writes — closed-world, no body Argument name: type Error signature optional payload Struct schema, states and attached operations; represents an entity, value or enum State named predicate over the instance _ Schema field name: type — every field required Lineage derives from { Source via carrier } — a struct of this model or an external struct; the lineage graph is a DAG Attachment nested in a struct or declared with on Struct; _ denotes the receiver Processing description level 0 prose · level 1 contract · level 2 flow — never pseudo-code Dependency graph depends on + flow callees; strict: acyclic · observed: cycles and ? candidates are findings Description (level 0) :: intent and rationale in prose — not checkable Contract (level 1) requires { p else Error } · ensures { result, old(x) } — checkable Flow (level 2) ordered steps: bind · write · raise · return — straight-line happy path, no expressions Edge kind contains · references · invokes · accesses · throws · implements · derives from — provenance: declared | derived | candidate Type string · number · boolean · vector<T> · { field: T } · Struct · (T) -> R · opaque · T? Domain model traceability model - bounded context · module - module · struct - entity, value, enum · operation - operation · error - error event; implicit by naming specy.ai
Click any node to jump to its definition. Download the diagram (SVG)

This file defines a foundational representation for describing data structures, operation signatures, global variable bindings, and the contracts and call flows that connect them. It is a description language, not a programming language: operations have no bodies — no statements, no branching on the happy path, no loops, no computed expressions. A Data & Operation model describes what data exists, what operations exist over that data, what each operation needs — other operations, global state — and what each operation guarantees. How an operation computes its result is deliberately out of scope; a flow, when present, says which declared operations it calls and in what order, never how the values in between are computed.

Conditions appear in exactly three places, all written in the same small predicate language: a struct’s named states (predicates over an instance), an operation’s contract (predicates over its arguments, its result and the globals it touches), and the guards on a flow’s failure exits. None of them is a computation — each is a boolean statement about data.

The Data & Operation model is the “how substrate” beneath the domain model (see DOMAIN-METAMODEL.md). Where the domain model captures business intent — entities, aggregates, commands, events — the Data & Operation model captures the structural skeleton of an implementation: concrete data shapes and the dependency graph of the operations that realize domain behavior. The two models are linked through optional, traceable implements relations (see Traceability to the Domain model).

The model serves two directions. Designed forward, by a person or an agent, it is written under the strict conformance level: its three graphs (struct references, struct lineage and operation dependencies) are acyclic and its effect declarations are closed-world, so topological ordering, impact analysis, dead-code detection and effect propagation are all decidable by simple graph traversal. Extracted backward, from an existing codebase (for instance by lowering a code graph), it is written under the observed level: the same syntax, but the rules that real code routinely violates — cycles, unresolved dispatch, types outside the closed type language — are reported as findings instead of rejected. One file format, two confidence levels.

Convention

Every concept in this metamodel carries a name, a description, and a metadata map (a set of arbitrary key/value pairs). These attributes are implicit and not repeated in each definition below.

Naming: struct names use PascalCase (Money, PriceEntry). Operation names, global binding names, and field names use camelCase (reserveStock, exchangeRates, unitPrice). Error signature names use PascalCase (InsufficientStock), mirroring the domain model’s error event naming. State labels are free-text string literals in business language ("a pending task").

Identity: an element is identified by the structured key (module, symbol, disambiguator?). The symbol is the dotted path below the module (Money, PriceEntry.effectivePrice, vatRate). The disambiguator exists only for overloaded operations and is the parameter type list, written in parentheses (addLine(OrderLine) vs addLine(string, number)). A rendered qualified name — pricing.PriceEntry.effectivePrice — is a display and reference form of that key; two elements are the same exactly when their keys are equal component by component.

Implicit implementation by naming: when a Data & Operation model element and a domain model element share the same name (compared case-insensitively, ignoring case-style differences such as reserveStock vs ReserveStock) within corresponding modules, the implements relation is presumed without being declared. An explicit implements declaration is only needed when names diverge — and it always wins over the naming presumption.

Evidence: a declaration may carry a // source: path:start-end comment naming the source location it was written from or extracted from. It is the same convention as every other Specy artifact. Under observed every declaration should carry one; under strict it is optional. It is a comment to the grammar and a source anchor to the tooling.

Concrete syntax appears throughout in fenced blocks. The normative grammar for it is src/grammars/data-operation.ebnf, which pairs with this document section by section: every production there carries a (metamodel: <Section>) back-reference to the section below that defines it. Constraints stated here as “shall” but not expressible in a context-free grammar (the acyclicity rules, the closed-world declaration rule, type conformance of initial values, flow/clause agreement) are listed as semantic rules at the foot of the grammar file, each tagged with the conformance level it applies to. The artifact extension is .dataop.

Conformance levels

A model declares its conformance level in its header: model pricing-engine strict { … } or model order-service observed { … }. When omitted, the level is strict.

strict (default) observed
Who writes it a designer or a design agent an extraction from code
Struct reference graph shall be acyclic cycles are findings
Lineage graph shall be acyclic; every edge declared cycles are findings; derived and candidate edges allowed
Operation dependency graph shall be acyclic cycles are findings
Effect declarations closed-world, complete complete as far as static analysis could see; blind spots are findings
Uncertain dependency or lineage source (?) not allowed allowed
opaque type not allowed allowed
External declarations allowed allowed
Flow guards on the happy path never never — a body’s branching that the flow cannot express is a finding, not a guard
Evidence comments optional expected on every declaration

Every syntactic construct is legal at both levels; the levels differ only in which semantic rules reject a model and which merely report on it. A model that passes strict validation also passes observed validation with no findings. Lowering a code graph into an observed model and then tightening it into a strict one is the intended path from “what the code does” to “what the design says”.

Relations:

  • 1..1 “governs” relation with a model

Model

A Data & Operation model is the top-level container. It groups modules and external declarations, fixes the conformance level, and optionally anchors the whole model to the domain layer it implements.

Relations:

  • 1..1 “has” relation with a conformance level (strict by default)
  • 1..n “contains” relation with modules
  • 0..n “declares” relation with external declarations
  • 0..1 “implements” relation with a domain bounded context or organization (the domain scope this model realizes)

Module

A module is the unit of decomposition, mirroring the domain model’s module concept. A module groups structs, global bindings, and operations that belong together. References that cross a module boundary shall be qualified with the module name (pricing.Money).

A module’s dependencies are derived, never declared: module A depends on module B when any struct, global binding, or operation of A references an element of B. Under strict the derived module dependency graph shall be acyclic — this follows from, and is checked alongside, the acyclicity of the struct reference graph and the operation dependency graph. Under observed a module cycle is a finding.

Relations:

  • 1..1 “belongs to” relation with a model
  • 0..n “declares” relation with structs
  • 0..n “declares” relation with global bindings
  • 0..n “declares” relation with operations
  • 0..n “declares” relation with external declarations
  • 0..n “depends on” relation with other modules (derived from element references)
  • 0..1 “implements” relation with a domain module (implicit by naming or explicit)

Composition: declarations as trait bundles

The surface syntax has a handful of keywords — module, struct, global, operation, external — and the writer never sees anything else. Behind each keyword, however, a declaration is not a node in a class hierarchy but a sum of traits: a small closed set of micro-capabilities, each contributing its attributes. The keyword picks the base bundle; the position of the declaration and the shape of its type can add traits to it. This is what lets one construct be several things at once without a hierarchy having to place it — a global whose type is a function is a value holder and invocable; an operation written inside a struct is invocable and attached to that struct.

Trait Attributes contributed Meaning
Named name has a name of its own
Module external is a unit of decomposition
Type external is a named data shape that other declarations can reference
WithChildren (marker) lexically contains declarations
ChildOf parent is lexically contained in a declaration
AttachedTo attachedTo semantically belongs to a struct (a method); may differ from parent
Structural (marker) holds a value; legal target of an access
Typed type has a declared type
WithValue initialValue has a declaration-site value
Invocable parameters, returnType, errors can be called
WithStates states has named derived conditions
WithContract requires, ensures has a contract
WithFlow flow has an ordered call flow
Declaration Traits
module Named, Module, WithChildren
struct Named, Type, WithChildren, WithStates
schema field Named, Structural, Typed, ChildOf
global Named, Structural, Typed, WithValue, ChildOf
global whose type is a function type the above plus Invocable
operation at module level Named, Invocable, WithContract, WithFlow, ChildOf
operation nested in a struct, or with on the above plus AttachedTo
external module Named, Module, with external = true
external struct Named, Type, with external = true

The trait vocabulary is closed and canonical. It is the level at which this model and a code graph extracted by static analysis meet: a lowering from such a graph maps each of its entities to the bundle above whose traits it carries, and a keyword is chosen from the bundle, never the other way round.

Relations as edge kinds

Every relation between declarations is one of seven edge kinds. The clauses of the surface syntax each emit exactly one kind, so the relation graph of a model is fully determined by its text.

Edge kind From → To Emitted by
contains WithChildren → ChildOf lexical nesting
references Typed → Type a field, parameter, return, global or function type naming a struct
invokes Invocable → Invocable depends on; a flow’s call steps
accesses (read, write, or both) Invocable → Structural reads, writes; a flow’s write steps
throws Invocable → error signature errors; a flow’s raise steps
implements any → domain element implements, represents, corresponds to, or the naming presumption
derives from Type → Type (a struct of the model → a struct or an external struct) derives from, one edge per source (see Lineage)

An edge may carry a provenance: declared (written in the model), derived (computed from other declared facts, such as a module dependency, or a lineage implied by a level-0 flow of the architecture model), or candidate (an uncertain dependency or lineage source marked ?, see Dependency graph and Lineage). Under strict every stored edge is declared.

Edges are stored in the outgoing direction only. Inverse views — the callers of an operation, the readers of a global, the operations attached to a struct, the structs derived from a struct — are derived and never written.

Data

Data in a Data & Operation model is JSON-like: the same scalar values as JSON, composed with vectors and maps. There are exactly seven type constructors and no others — no unions, no inheritance, no generics beyond the vector element type.

Type

A type is one of the following constructors:

Constructor Form Meaning
Scalar string, number, boolean The JSON scalar values. number covers integers and decimals alike, as in JSON.
Vector vector<T> An ordered sequence whose elements all conform to the single element type T. Vectors are homogeneous — a vector shall not mix element types.
Map { field: T, … } A record of named fields, each with its own type. Every field is required — there are no optional fields.
Struct reference StructName A reference to a defined struct or external struct, possibly qualified (pricing.Money).
Function (T, U) -> R, (T) -> void A value that can be called: the parameter types in order and the return type, or void for a function that exists for its effects. This is what lets a global binding hold an operation.
Opaque opaque "…" A type the closed type language cannot express, carried as the verbatim signature string of the source it was extracted from (opaque "java.util.Map<String, Object>"). observed only. Nothing can be checked about an opaque value except that it is passed around whole.
Nullable marker T? Admits null in addition to the values of T. null is not a standalone type — it exists only through this marker. Applicable to any type.

Constraints:

  • A type shall not be a union: a field, argument, return, or vector element has exactly one type.
  • Absence of a value shall be expressed with the nullable marker, never by omitting a field: a map value always carries all declared fields, some of which may hold null when their type is nullable.
  • opaque shall not appear in a strict model. Tightening an observed model into a strict one means replacing every opaque type with a struct that describes the shape actually used.

The type language is deliberately closed. Its purpose is to make every data shape in the model fully explicit and structurally comparable — two types are the same exactly when their constructor trees are the same. Opaque is the one escape hatch, and it is confined to extracted models precisely so that designed ones stay comparable.

Struct

A struct is a named data structure: the model’s unit of vocabulary. Arguments, returns, error payloads, and global bindings refer to data shapes by struct name rather than repeating structural types. A struct groups its data shape, the named conditions of an instance, and the operations that belong to it:

  • a mandatory schema block — the fields, each a name and a type. This is the shape (a map type), the same field-list machinery as an inline map, given a name.
  • an optional derives from clause — the structs its data is derived from: its lineage.
  • an optional state block — zero or more named states, each a boolean predicate over an instance.
  • zero or more nested operations — operations written inside the struct, which are thereby attached to it. Within a nested operation _ denotes the receiver instance, exactly as it does in a state predicate.

Constraints:

  • Under strict, a struct’s schema block shall only reference previously defined structs. There are no forward references and no recursion — a struct shall not reference itself directly or transitively. The struct reference graph is therefore a DAG, and every value described by the model is finite by construction. Under observed a cycle in the struct reference graph is a finding.
  • The previous-definition rule applies to the schema block only. A nested operation’s signature may reference any struct of the model.
  • A struct name shall be unique within its module.

Example.

struct Money {
  schema {
    amount:   number
    currency: string
  }
}

struct Task {
  schema {
    name:   string
    status: string
  }

  state {
    "a pending task"   : _.status = "pending"
    "a completed task" : _.status = "done"
  }

  operation complete()
    writes { _.status }
    requires { _.status = "pending" }
    ensures  { _.status = "done" }
}

struct PriceList {
  schema {
    name:    string
    entries: vector<PriceEntry>
  }
}

Relations:

  • 1..1 “belongs to” relation with a module
  • 1..1 “is shaped by” relation with a schema block (a map type)
  • 0..n “declares” relation with states
  • 0..n “owns” relation with attached operations (nested, or declared elsewhere with on)
  • 0..n “references” relation with structs (under strict: previously defined ones only, forming a DAG)
  • 0..n “derives from” relation with structs and external structs (one lineage per source; the lineage graph is a DAG)
  • 0..1 “represents” relation with a domain entity, value type, or enum (implicit by naming or explicit)

State

A state is a named, derived condition of a struct instance: a business-language label bound to a boolean predicate evaluated against the instance’s own data. Within a predicate, _ denotes the instance and _.field (a dot-path) reaches its fields. The predicate language is small and total — comparisons (=, !=, >, <, >=, <=) between a path and a literal or between two paths, is null / is not null, membership (in { … }), and the boolean connectives and / or / not. It has no function calls, no arithmetic, and — in a state — no reference to anything outside the instance. The same predicate language, with more roots in scope, is what an operation’s contract uses.

States are derived, not stored. A struct has no mutable “current state” field, no transitions, and no start state — a state simply holds or does not hold for a given instance, computed from its data. Two states may hold at once; they need not partition the instance space. This is the deliberate difference from the domain model’s Entity state machine (see DOMAIN-METAMODEL.md, State machine), which models stored, mutually exclusive lifecycle states connected by guarded transitions. Here, states are a read-only vocabulary for talking about data.

Relations:

  • 1..1 “belongs to” relation with a struct
  • 1..1 “is defined by” relation with a boolean predicate over the instance (_)

External declaration

An external declaration names a struct or a module that the model references but does not describe: a library type, a platform package, a service owned by another model. It is the closure device — every reference in the model resolves to something declared, and an external declaration is the honest way to declare what lies outside the model’s scope.

  • external struct java.util.List declares a struct with a name and nothing else: no schema, no states, no lineage, no operations. It can be referenced wherever a struct reference is legal and nothing can be said about its shape — it behaves as a nominal type. A struct described by another Data & Operation model is declared the same way, under its own module name (external struct catalog.Product); when the two models are analysed together it is identified with that model’s struct by its key (module, symbol) (see Convention).
  • external module java.util declares a module with a name and no declarations. It exists so that external structs have a module to belong to, and so that derived module dependencies on it are visible.

Exactly two things are external-declarable: types and modules. An external operation or global is deliberately not expressible — a call into a library is a dependency on the library’s module, recorded by the referencing declaration’s derived module dependency, not a fabricated operation with an invented signature.

External declarations are legal at both conformance levels. A designed model may honestly depend on a library type; an extracted one always does.

Relations:

  • 1..1 “belongs to” relation with a model or a module
  • 0..n “referenced by” relation with typed declarations (derived)
  • 0..n “source of” relation with structs (derived: the inverse of lineage)

Lineage

A lineage states that a struct’s data is derived from data held elsewhere: a reporting table built from orders, a search index over products, the local copy of a read-only entity mastered in another context. It is written in the derived struct, after its schema, as a derives from clause listing the sources:

external module catalog
external struct catalog.Product

module pricing {

  struct Product {
    schema {
      productId: string
      name:      string
    }
    derives from { catalog.Product via "flow 12" }
  }

  struct ProductRevenue {
    schema {
      productId: string
      revenue:   Money
    }
    derives from { Product, ordering.Order via OrderEvents }
  }
}

Each source is one lineage: a derives from edge, stored outgoing from the derived struct (see Relations as edge kinds). A source is either:

  • a struct of the model, unqualified within its module and qualified across a module boundary (ordering.Order, ordering being another module of the same model);
  • an external struct, which is how a struct described by another model is named (catalog.Product, see External declaration). The other model is not named, only the struct’s key; when the models are analysed together, the key joins them and the lineage graph continues across the boundary.

A source may name its carrier with via:

  • a string, for a carrier this model has no element to name: conventionally a level-0 flow of the architecture model, cited by its number (via "flow 12", see SOFTWARE-ARCHITECTURE-METAMODEL.md, Flow), or a medium (via "nightly CSV export");
  • a name, for an interface or a channel of the architecture model (via OrderEvents), resolved when that model is analysed alongside.

Lineage is struct-level. Which field comes from which field is out of scope, as it is in the architecture model: a field mapping would need paths rooted in a source struct, which the predicate language does not have, and as soon as a field is computed rather than copied (a revenue summed over order lines) it would need an expression, which nothing in this metamodel has.

Provenance. A lineage edge carries the provenance every edge carries:

  • Under strict every lineage is declared. A designer commits to where data comes from, and a tool never adds lineage to a designed model: that quotePrice returns a Money computed from a PriceEntry does not make that Money a copy of a PriceEntry.
  • Under observed an extraction may write a candidate lineage, marked ? like an uncertain dependency: derives from { Order? }. The usual evidence is an operation that receives or reads a value of one struct and writes a global or a receiver field that holds another. A flow makes the inference mechanical: a flow has no expressions, so the value of a write step leads back through the bind steps to the names it was made from, and each of those typed by another struct is a candidate source. Two structs are excluded: the written struct itself (an update from its previous value is not a derivation) and the structs its schema references (Money in Invoice.total is shape, not origin). What the flow shows is evidence of a derivation, not proof of a copy; hence candidate, and an evidence comment naming the operation.
  • A lineage is derived when it follows from facts declared in other models: a level-0 flow carries X as Y into the system this model details, a struct of this model represents Y, and a struct of the model or an external struct corresponds to X, by represents or by naming. Under observed a tool may add such an edge. Under strict a level-0 flow with no matching declared lineage is a traceability gap, reported by audit.

Constraints.

  • No struct shall derive from itself, directly or transitively: the lineage graph is a DAG. Under strict a cycle is rejected; under observed it is a finding, typically a two-way synchronisation, which means two places where the same truth can be changed.
  • A source shall resolve to a struct of the model or to an external struct, never to an external module, a global binding or an operation. A source appears at most once in a clause.
  • An external struct may be a source, never a derived struct: it has no body to write a lineage in, and where its data comes from is said by the model that describes it.
  • ? on a source shall not appear in a strict model.
  • The lineage graph is separate from the struct reference graph: a struct may derive from a struct its schema does not reference, and sources are exempt from the previous-definition rule of the schema block. A source in another module is still a reference: it makes the deriving module depend on that module (see Module).

Checks enabled. Each check is a query over the lineage graph, the domain links and, where one exists, the enterprise model (ENTERPRISE-ARCHITECTURE-METAMODEL.md, Data package). None is a parse-time rule. Under observed each runs on the declared and derived edges, then again with the candidates, so that a result resting on a candidate shows as one.

  • Impact. The impact set of a struct is every struct that derives from it, directly or transitively, together with every operation that references, reads or writes one of them (the reverse reachability of the dependency graph). Across models, the walk continues through external structs, matched by key.
  • Classification propagation. The Data & Operation model carries no classification; the enterprise model classifies schemas. When it does, a struct derived from a personal struct is personal unless a decision says otherwise, and a derived struct classified differently with no such decision is reported.
  • Provenance. Every struct that has identity (it represents an entity, an aggregate or a read-only entity) and every struct a global binding holds (as its type or as its vector element type) is expected to have a system of record: it represents an entity or an aggregate of the context the model implements, other than a read-only entity, or it derives, directly or transitively, from a struct that does. An external source ends the walk inside the model: it is presumed mastered where it is described, and the walk continues there when that model is analysed alongside. A struct with neither a master nor a lineage is reported: nothing says where its truth lives.

Traceability. Two other metamodels state the same fact at a coarser grain:

  • The domain model’s read-only entity (DOMAIN-METAMODEL.md, Read-only Entity) is a lineage seen from the consumer’s side, one hop long, between contexts: sourced-from names the upstream context or external system, not a struct. A struct that represents a read-only entity is therefore a derived struct: it should derive from at least one struct, and its sources should belong to the upstream that sourced-from names. Both are audited, not presumed. Lineage continues where sourced-from stops, since the upstream struct may itself be derived.
  • The architecture model’s level-0 flow (SOFTWARE-ARCHITECTURE-METAMODEL.md, Flow) states it between systems and between domain concepts: flow 2 "Accounts" carries Account as Customer says that customers are made from accounts. A lineage states it between structs and may cite the flow as its carrier (via "flow 2"). When it does, the two should agree: the source corresponds to the concept the flow carries, and the derived struct to the concept it becomes (the as target, or the carried concept itself when there is no as).

Relations:

  • 1..1 “belongs to” relation with a struct of the model (the derived struct; never an external struct)
  • 1..1 “derives from” relation with a source: a struct of the model or an external struct
  • 0..1 “carried by” relation with a carrier: a label, or an interface or a channel of the architecture model
  • 1..1 “has” relation with a provenance: declared, derived or candidate

Global Binding

A global binding is a named, mutable, typed variable at module level. It declares a piece of shared state that operations may read or write. A global binding has exactly one type and exactly one initial value that shall conform to the declared type:

  • for any type but a function type, the initial value is a literal, written in JSON notation;
  • for a function type, the initial value is the name of an operation whose signature conforms to the function type. The global then holds that operation: it is a value holder and invocable, and it may be called through the binding by any operation that declares it in reads.

The model therefore fully describes the starting state of the world, including which operation each function-valued global initially holds.

Global bindings are mutable, but mutation is never ambient: a global binding changes only through operations that explicitly declare it in their writes list (see Operation). Which operations touch which globals is thus a total, declared fact — the read/write access matrix of the model is closed and analyzable. A function-valued global is how the model expresses a swappable strategy without giving up that closure: rebinding it is a write, calling it is a read, and the effective dependency at the initial state is derivable from the declared initial value.

Example.

global standardPriceList: PriceList = {
  "name": "standard",
  "entries": []
}

global vatRate: number = 0.20

global rounding: (Money) -> Money = roundHalfUp

Relations:

  • 1..1 “belongs to” relation with a module
  • 1..1 “is typed by” relation with a type
  • 1..1 “is initialized with” relation with a literal value, or an operation when the type is a function type (shall conform to the type)
  • 0..n “read by” relation with operations (derived from operations’ reads declarations)
  • 0..n “written by” relation with operations (derived from operations’ writes declarations)

Operation

An operation is a signature with declared needs and guarantees: a name, typed arguments, at most one typed return, declared error signatures, and — beyond the signature — a complete declaration of what it needs:

  • depends on — the other operations it calls upon to do its job;
  • reads — the global bindings whose value it consults, and, for an attached operation, the receiver fields it reads;
  • writes — the global bindings it mutates, and, for an attached operation, the receiver fields it mutates.

These declarations are closed-world: an operation shall declare every operation it directly depends on and every global binding or receiver field it directly reads or writes. Nothing is accessed that is not declared. Transitive access is not re-declared — it is derivable by walking the dependency graph, which is the point: the direct declarations plus the graph give complete effect information for free.

An operation carries no body. What it may carry instead is a description of its processing at one of three levels — prose, contract, flow — defined in Describing processing. None of the three is a computation.

An operation with no return type exists for its effects; such an operation shall declare at least one writes — an operation with neither a return nor a write does nothing observable and shall not exist.

Relations:

  • 1..1 “belongs to” relation with a module
  • 0..1 “attached to” relation with a struct (see Attachment)
  • 0..n “has” relation with arguments (each argument: a name and a type)
  • 0..1 “returns” relation with a type
  • 0..n “declares” relation with error signatures
  • 0..n “depends on” relation with other operations (declared or candidate; under strict, shall form a DAG — see Dependency graph)
  • 0..n “reads” relation with global bindings and receiver fields
  • 0..n “writes” relation with global bindings and receiver fields
  • 0..n “requires” relation with preconditions
  • 0..n “ensures” relation with postconditions
  • 0..1 “describes its processing with” relation with a flow
  • 0..1 “implements” relation with a domain operation (implicit by naming or explicit)

Example.

operation quotePrice(productId: string, quantity: number): Money
  errors {
    UnknownProduct
  }
  depends on { findPriceEntry, applyVat }
  reads { vatRate }
  requires { quantity > 0 }

operation registerDiscount(productId: string, rate: number)
  errors {
    UnknownProduct
    InvalidRate(min: number, max: number)
  }
  depends on { findPriceEntry }
  writes { standardPriceList }
  requires { rate >= 0 and rate <= 1 else InvalidRate }
  implements pricing.GrantProductDiscount

Attachment

An operation may be attached to a struct: it is then a method of that struct, and within its clauses and predicates _ denotes the receiver instance. Attachment is declared in one of two ways:

  • by nesting — the operation is written inside the struct’s block. Its lexical parent and its attachment are the same struct.
  • by the on clause — the operation is written at module level (or inside another struct) with on StructName after its return type. Its lexical parent is where it is written; its attachment is the named struct. This is how an extension method, a receiver method declared in a separate file, or a Clojure extend-type is described without moving it.

A nested operation without on is attached to its enclosing struct. A nested operation with on is attached to the named struct, not the enclosing one — this is the one case where containment and attachment diverge, and the model keeps both.

An attached operation is referenced from elsewhere by Struct.operation (PriceEntry.effectivePrice), qualified by module when it crosses a module boundary. Inside the struct it is attached to, the unqualified name suffices.

An attached operation may declare receiver fields in reads and writes as _.field paths. Under the closed-world rule, an attached operation that mutates its receiver shall say so, field by field.

Relations:

  • 0..1 “attached to” relation with a struct
  • 1..1 “lexically contained in” relation with a module or a struct

Error signature

An error signature declares a named failure mode of an operation, with an optional data payload describing what accompanies the failure. Errors are part of the signature — a caller knows every way an operation can fail by reading its declaration, without any body to inspect.

The payload, when present, is a map of named typed fields (or a struct reference), following the same type language as everything else.

An error signature may be bound to a precondition: requires { p else E } states that the violation of p is signalled as E (see Level 1 — Contract). An error not bound to any precondition is raised on a condition the contract does not express — typically a failure discovered by a dependency, such as UnknownProduct surfacing from findPriceEntry.

When an operation implements a domain operation, its error signatures correspond to the domain’s error events: an error raised by the implementing operation surfaces as the matching Error Event in the domain model (see DOMAIN-METAMODEL.md, Error Event). The correspondence follows the same implicit-by-naming rule as implements.

Relations:

  • 1..1 “belongs to” relation with an operation
  • 0..1 “carries” relation with a payload type
  • 0..n “signals the violation of” relation with preconditions
  • 0..1 “corresponds to” relation with a domain error event (implicit by naming or explicit)

Describing processing

An operation never has a body, but it does need to say what it does. The metamodel offers three levels, each optional, each strictly more constrained than the previous, and each analyzable to the degree it is constrained. Pseudo-code is deliberately absent: it has no grammar to check against, no semantics two readers agree on, and it duplicates the real body one abstraction level up.

Level Construct Checkable Says
0 :: "description" no why the operation exists and the rationale behind its rules
1 requires / ensures yes what must hold before the call and what is guaranteed after
2 flow yes which declared operations are called with what, in what order, and where the failure exits are

Level 1 is the default. Every operation that implements a domain operation, and every operation that writes, should carry a contract. Level 2 is for orchestrating operations — those whose value lies in the sequence of calls they make, typically application-service operations with several dependencies. Level 0 is for the reason behind a rule, which no predicate expresses. A leaf operation such as applyVat may legitimately carry none of the three: its signature and its reads already say everything a description language should.

Level 0 — Description

The description every concept carries (the :: operator). One or two sentences of intent: “Rounds half-up because the ledger does.” It is the right place for rationale and the wrong place for sequence — “first we look up the entry, then we apply VAT” is a flow written in a form nothing can check, and belongs at level 2.

Level 1 — Contract

A contract is a set of preconditions (requires) and postconditions (ensures), each a predicate in the state predicate language with a wider set of roots in scope:

Root In scope in Denotes
an argument name requires, ensures, flow guards the argument’s value at the call
_ attached operations only the receiver instance (its state after the call, in ensures)
a global in reads or writes requires, ensures, flow guards the global’s value (after the call, in ensures)
result ensures only the returned value
old(_), old(g) ensures only the receiver’s or the global’s value before the call

A precondition is an obligation on the caller. It may be bound to an error signature with else: requires { rate >= 0 and rate <= 1 else InvalidRate } states that the operation checks the condition and signals InvalidRate when it fails. A precondition without else is a pure contract — its violation is a caller defect, not a signalled failure. Every error named by an else shall be declared in the operation’s errors.

A postcondition is a guarantee. It relates the result, the receiver and the written globals after the call to the arguments and to the old values before it. It may only mention globals the operation declares in reads or writes, and old may only be applied to _ or to a global in writes — there is no “before” to speak of for anything else.

Contracts mirror the domain model’s preconditions and postconditions on operations (see DOMAIN-METAMODEL.md, Precondition and postcondition). When an operation implements a domain operation, the two contracts should be compatible: the implementing precondition no stronger, the implementing postcondition no weaker. Contract compatibility is a traceability audit, not a parse-time rule.

Example.

operation registerDiscount(productId: string, rate: number)
  errors {
    UnknownProduct
    InvalidRate(min: number, max: number)
  }
  depends on { findPriceEntry }
  writes { standardPriceList }
  requires {
    rate >= 0 and rate <= 1 else InvalidRate
  }
  ensures {
    standardPriceList.name = old(standardPriceList).name
  }

Relations (precondition):

  • 1..1 “belongs to” relation with an operation
  • 1..1 “is defined by” relation with a predicate over the arguments, the receiver and the readable globals
  • 0..1 “signals” relation with an error signature (else)

Relations (postcondition):

  • 1..1 “belongs to” relation with an operation
  • 1..1 “is defined by” relation with a predicate over the result, the arguments, the receiver, the declared globals and their old values

Level 2 — Flow

A flow is an ordered list of steps describing the calls an operation makes. It is a sequence diagram in text, not an algorithm. Every step is one of exactly four forms:

Step Form Meaning
bind name = callee(args) call a declared dependency and name its result
write write target = value [when p] store a value into a global or a receiver field
raise raise Error(payload) [when p] signal a declared error
return return value end with the named value

Where callee is an operation of the model — module-level, Struct.op, binding.op(…) for a method on a bound value, _.op(…) for a method on the receiver, or a function-valued global — and every argument, value or payload field is a name in scope (a parameter, an earlier binding, _, a global in reads), a dot-path from one, or a literal.

Two rules keep a flow a description rather than a program:

  • No expressions. A flow computes nothing: there is no arithmetic, no comparison outside a guard, no construction of values except by calling an operation that returns one. If a computation matters enough to mention, it gets a name and becomes an operation. This is a feature: it forces the decomposition into the open, where the dependency graph can see it.
  • The happy path is straight-line. A when guard is legal only on write and raise steps. A bind step is never conditional and there is no branch, loop or alternative on the success path. If the success path genuinely branches, the branch lives inside a callee. Guarded failure exits are the one piece of control flow worth keeping, because they are exactly what a throws edge with a source anchor is evidence of.

Agreement with the clauses. When a flow is present, the operation’s depends on, reads, writes and errors are derivable from it: every callee is a dependency, every write target is a write, every name read is a read, every raise is an error. The declared clauses and the flow shall agree — a callee absent from depends on, or a declared dependency the flow never calls, is a defect. In practice the writer keeps the clauses and lets the tool check them, or omits them and lets the tool fill them in.

Extraction. A flow is what static analysis can honestly recover from a body: the outgoing invocations in source order give the bind steps, the assignments to globals and fields give the write steps, the throw sites give the raise steps. What it cannot recover — the branching of the success path — is what the observed level reports as a finding rather than smuggles into the flow as a guard.

Example.

operation quotePrice(productId: string, quantity: number): Money
  errors { UnknownProduct }
  depends on { findPriceEntry, PriceEntry.effectivePrice, scale, applyVat }
  reads { vatRate }
  requires { quantity > 0 }
  ensures  { result.currency = "EUR" }
  flow {
    entry = findPriceEntry(productId)
    unit  = entry.effectivePrice()
    gross = scale(unit, quantity)
    net   = applyVat(gross, vatRate)
    return net
  }

operation registerDiscount(productId: string, rate: number)
  errors { UnknownProduct, InvalidRate(min: number, max: number) }
  depends on { findPriceEntry, withDiscount }
  reads { standardPriceList }
  writes { standardPriceList }
  requires { rate >= 0 and rate <= 1 else InvalidRate }
  flow {
    raise InvalidRate(min: 0, max: 1) when rate < 0 or rate > 1
    entry = findPriceEntry(productId)
    write standardPriceList = withDiscount(standardPriceList, entry, rate)
  }

Reading the second flow: UnknownProduct never appears in a raise, yet it is a declared error — it surfaces from findPriceEntry. A flow declares the failures the operation itself signals; failures propagated from dependencies are declared in errors and derivable from the callees’ signatures.

Relations (flow):

  • 1..1 “belongs to” relation with an operation
  • 1..n “consists of” relation with steps, ordered
  • 0..n “calls” relation with operations (derived: one invokes edge per bind step)
  • 0..n “writes” relation with globals and receiver fields (derived: one accesses edge per write step)
  • 0..n “raises” relation with error signatures (derived: one throws edge per raise step)

Dependency graph

The depends on declarations of all operations — together with the callees of every flow, which shall agree with them — form the model’s dependency graph.

A dependency may be marked uncertain with a trailing ?: depends on { render?, renderHtml?, renderPdf? }. An uncertain dependency records that the call site was observed but its target could not be resolved to a single operation — dispatch through an interface, a dynamic call, a function-valued global whose current value is unknown. Its edge carries the candidate provenance. Uncertain dependencies are observed only: a designed model commits to what it calls.

Under strict the graph shall be acyclic: no operation depends on itself, directly or transitively — recursion and mutual recursion are not expressible. Together with the closed-world declaration rule, acyclicity makes the model fully analyzable by topological traversal:

  • Effective reads/writes of an operation = its declared reads/writes ∪ the effective reads/writes of everything it depends on.
  • Effective errors of an operation = its declared errors, which shall include every error its dependencies can surface that it does not itself handle. The model does not express handling; the declared set is the caller’s contract.
  • Impact of changing a struct or global = the reverse reachability set in the graph.
  • Build/evaluation order = any topological order of the graph.

Under observed a cycle is a finding, reported with the operations on it, and the analyses above are computed on the condensation of the graph (strongly connected components collapsed) so that they remain well-defined.

Constraints:

  • Under strict, the operation dependency graph shall be acyclic; under observed, a cycle is a finding.
  • An operation shall declare every operation it directly depends on (no undeclared calls).
  • An operation shall declare every global binding and receiver field it directly reads or writes (no ambient state access).
  • When a flow is present, its callees, write targets, read names and raised errors shall agree with the declared clauses.
  • ? shall not appear in a strict model.

Traceability to the Domain model

The Data & Operation model is linked to the domain model through optional, per-element implements/represents/corresponds to relations. Each relation is implicit when names match (see Convention) and explicit otherwise; explicit declarations always override the naming presumption. All links are optional — a model may describe purely technical machinery with no domain counterpart.

Data & Operation concept Domain concept Relation Implicit rule
Model Bounded context / organization implements model name matches context shortname
Module Module implements same module name
Struct Entity, value type, or enum represents same name (Money ↔ value Money)
Lineage Read-only entity’s sourced-from agrees with none: a struct that represents a read-only entity derives from a struct of the upstream its sourced-from names; audited, not presumed (see Lineage)
Operation Operation implements same name, case-style ignored (reserveStock ↔ ReserveStock); an attached operation matches an operation owned by the represented entity
Precondition / postcondition Precondition / postcondition refines via the enclosing operation’s implements; compatibility audited, not presumed
Error signature Error event corresponds to same name

Traceability is directional: the Data & Operation model points at the domain model, never the reverse — the domain model remains free of implementation concerns. Coverage questions (“which domain operations have no implementing operation?”) are answered by inverting the links mechanically. Because an observed model carries the same links, the same inversion answers “which domain operations does the code not implement?” directly from an extraction.

Worked example: a designed model (strict)

A single-module model implementing part of a pricing domain module. The level is strict by default; the header could equally say so.

model pricing-engine implements pricing {

  external module catalog
  external struct catalog.Product

  module pricing {

    struct Money {
      schema {
        amount:   number
        currency: string
      }
    }

    struct Product {
      schema {
        productId: string
        name:      string
      }
      derives from { catalog.Product via "flow 12" }
    }

    struct PriceEntry {
      schema {
        productId:    string
        unitPrice:    Money
        discountRate: number?
      }

      state {
        "discounted" : _.discountRate is not null
        "full price" : _.discountRate is null
      }

      operation effectivePrice(): Money
        :: "The unit price after discount, before VAT."
        depends on { discount }
        reads { _.unitPrice, _.discountRate }
        ensures { result.currency = _.unitPrice.currency }
    }

    struct PriceList {
      schema {
        name:    string
        entries: vector<PriceEntry>
      }
    }

    global standardPriceList: PriceList = {
      "name": "standard",
      "entries": []
    }

    global vatRate: number = 0.20

    global rounding: (Money) -> Money = roundHalfUp
      :: "Rounds half-up because the ledger does."

    operation roundHalfUp(price: Money): Money
      ensures { result.currency = price.currency }

    operation discount(price: Money, rate: number?): Money
      ensures { result.currency = price.currency }

    operation scale(price: Money, factor: number): Money
      requires { factor > 0 }
      ensures  { result.currency = price.currency }

    operation applyVat(price: Money, rate: number): Money
      depends on { roundHalfUp }
      reads { rounding }
      ensures { result.currency = price.currency }

    operation findPriceEntry(productId: string): PriceEntry
      errors { UnknownProduct }
      reads { standardPriceList }
      ensures { result.productId = productId }

    operation withDiscount(list: PriceList, entry: PriceEntry, rate: number): PriceList
      ensures { result.name = list.name }

    operation quotePrice(productId: string, quantity: number): Money
      errors { UnknownProduct }
      depends on { findPriceEntry, PriceEntry.effectivePrice, scale, applyVat }
      reads { vatRate }
      requires { quantity > 0 }
      flow {
        entry = findPriceEntry(productId)
        unit  = entry.effectivePrice()
        gross = scale(unit, quantity)
        net   = applyVat(gross, vatRate)
        return net
      }

    operation registerDiscount(productId: string, rate: number)
      errors {
        UnknownProduct
        InvalidRate(min: number, max: number)
      }
      depends on { findPriceEntry, withDiscount }
      reads { standardPriceList }
      writes { standardPriceList }
      requires { rate >= 0 and rate <= 1 else InvalidRate }
      ensures  { standardPriceList.name = old(standardPriceList).name }
      flow {
        raise InvalidRate(min: 0, max: 1) when rate < 0 or rate > 1
        entry = findPriceEntry(productId)
        write standardPriceList = withDiscount(standardPriceList, entry, rate)
      }
      implements pricing.GrantProductDiscount
  }
}

Reading the graph: quotePrice never declares standardPriceList or rounding, yet its effective reads include both — derived through findPriceEntry and applyVat. applyVat reads rounding and depends on roundHalfUp: the read is the call through the function-valued global, the dependency is the operation the global initially holds, and the two agree. UnknownProduct raised by quotePrice is its own declared error even though the underlying failure originates in findPriceEntry: every operation declares the full set of errors it can surface. registerDiscount is the only operation that can change any state, and the model says so explicitly — its flow, its writes and its postcondition all say the same thing from three angles. Product is the pricing engine’s copy of the catalog’s product: its lineage names the source by its key, catalog.Product, declared external because this model does not describe it, and cites the level-0 flow that carries it. If the pricing domain declares Product as a read-only entity sourced from the catalog context, the struct represents it by naming, the provenance check is satisfied through the lineage, and the catalog model, analysed alongside, says which aggregate masters the product.

Worked example: an extracted model (observed)

The same syntax, lowered from a Java codebase by static analysis. Everything the strict level forbids is legal here and becomes a finding: the opaque field, the uncertain dispatch, the guessed lineage of Invoice, the cycle between bill and render. Every declaration carries its evidence.

model order-service observed implements ordering {

  external module java.util
  external struct java.util.List
  external struct java.util.Map

  module com.acme.order {

    struct Money {
      // source: src/main/java/com/acme/order/Money.java:8-40
      schema {
        amount:   number
        currency: string
      }
    }

    struct OrderLine {
      // source: src/main/java/com/acme/order/OrderLine.java:10-52
      schema {
        productId: string
        qty:       number
        unitPrice: Money
      }

      operation subtotal(): Money
        // source: src/main/java/com/acme/order/OrderLine.java:38-44
        reads { _.qty, _.unitPrice }
    }

    struct Order represents ordering.Order {
      // source: src/main/java/com/acme/order/Order.java:14-88
      schema {
        id:    string
        lines: vector<OrderLine>
        meta:  opaque "java.util.Map<String, Object>"
      }

      operation total(): Money
        // source: src/main/java/com/acme/order/Order.java:52-61
        depends on { OrderLine.subtotal }
        reads { _.lines }
        // FINDING: body iterates over `_.lines` (cyclomatic 2); no straight-line flow can show it

      operation addLine(line: OrderLine)
        // source: src/main/java/com/acme/order/Order.java:63-67
        writes { _.lines }

      operation addLine(productId: string, qty: number)
        // source: src/main/java/com/acme/order/Order.java:69-74
        depends on { addLine(OrderLine) }
        writes { _.lines }
    }

    struct Invoice {
      // source: src/main/java/com/acme/order/Invoice.java:9-33
      schema {
        orderId: string
        total:   Money
      }
      derives from { Order? }
      // FINDING: candidate lineage, inferred from `bill`: it receives an Order and writes `invoices`, which holds Invoices
    }

    global invoices: vector<Invoice> = []
      // source: src/main/java/com/acme/order/OrderService.java:22

    operation bill(order: Order): Invoice
      // source: src/main/java/com/acme/order/OrderService.java:40-77
      errors { PaymentDeclined corresponds to ordering.PaymentRefused }
      depends on { Order.total, render? }
      reads { invoices }
      writes { invoices }
      flow {
        amount  = order.total()
        // FINDING: `Renderer.render` dispatched through an interface; two implementations in scope
        raise PaymentDeclined when amount.amount > 10000
      }
      // FINDING: cycle bill -> render -> bill

    operation render(invoice: Invoice): string on Invoice
      // source: src/main/java/com/acme/order/InvoiceRenderer.java:18-45
      depends on { bill? }
  }
}

Four of the findings are what an observed validation reports on this file: the cycle, the two candidate dependencies, the candidate lineage of Invoice, and the loop in total that no straight-line flow can show. Tightening the model into strict means resolving each — replacing the interface dispatch with the concrete callee or a function-valued global, breaking the cycle, confirming the lineage (an invoice is made from an order, so the ? goes) or dropping it, replacing opaque with the shape actually used, and lifting the loop into a named operation (sumSubtotals(lines: vector<OrderLine>): Money) so the flow can call it. Each of those is a design decision the extraction surfaced rather than made.