LLMs as drafting assistants
A language model can suggest a metamodel, an example model or an Epsilon program. Its answer is a draft: the text may parse yet express the wrong domain rule, and a plausible explanation may disagree with the model. Use the metamodel to define structure, EVL to state constraints, and runnable examples to test the result. An LLM can help propose alternatives; evidence from the model decides which alternative works.
Adapted from Dimitris Kolovos, Metamodelling with ChatGPT, Episode 2. In that experiment, an example of Emfatic helped the language model produce a syntactically usable draft; later feedback improved a generated EGL template. Our drafts below are illustrations written for this tutorial, not recorded outputs from that experiment or a live LLM. No example calls an LLM service.
Review a metamodel draft
The first illustrative draft widens Connector.source and Connector.target to the abstract Port type. It therefore accepts an input port as a source and an output port as a target. The EOL query runs successfully and prints a reversed connection. A successful parse and run show that the files are usable by Epsilon; they do not prove that the intended wiring rule holds.
for (c in Connector.all) {
(c.source.eContainer().name + "." + c.source.name + " -> " + c.target.eContainer().name + "." + c.target.name).println();
}
More files: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" type="DIGITAL"/>
<inPort name="window" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" type="DIGITAL"/>
<outPort name="sound" type="DIGITAL"/>
</component>
<component name="Siren">
<inPort name="sound" type="DIGITAL"/>
</component>
<connector source="Alarm.AndGate.open" target="Alarm.OrGate.open"/>
<connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
@namespace(uri="components", prefix="components")
package components;
enum PortType {
DIGITAL;
ANALOG;
}
class Architecture {
attr String[1] name;
val Component[*]#architecture components;
val Connector[*] connectors;
}
class Component {
attr String[1] name;
ref Architecture#components architecture;
val Port[*] ports;
}
abstract class Port {
attr String[1] name;
attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}
class Connector {
ref Port source;
ref Port target;
}
AndGate.open -> OrGate.open
AndGate.sound -> Siren.sound
Ask a drafting assistant for the classes, containment, multiplicities, endpoint direction and one valid and one invalid example. Review each answer against domain requirements. In particular, check val versus ref: connectors refer to ports owned by components; they do not contain new copies of those ports.
Check a model draft
EVL can make the direction rule executable. The next check rejects the reversed connector even though the broad draft metamodel accepts it. In the Playground, EVL findings appear as notes on the model diagram; the captured result below records the same validation run.
context Connector {
constraint OutputToInput {
check: self.source.isTypeOf(OutPort) and self.target.isTypeOf(InPort)
message: "Connector endpoints must run from OutPort to InPort"
}
}
More files: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" type="DIGITAL"/>
<inPort name="window" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" type="DIGITAL"/>
<outPort name="sound" type="DIGITAL"/>
</component>
<component name="Siren">
<inPort name="sound" type="DIGITAL"/>
</component>
<connector source="Alarm.AndGate.open" target="Alarm.OrGate.open"/>
<connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
@namespace(uri="components", prefix="components")
package components;
enum PortType {
DIGITAL;
ANALOG;
}
class Architecture {
attr String[1] name;
val Component[*]#architecture components;
val Connector[*] connectors;
}
class Component {
attr String[1] name;
ref Architecture#components architecture;
val Port[*] ports;
}
abstract class Port {
attr String[1] name;
attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}
class Connector {
ref Port source;
ref Port target;
}
Error [OutputToInput]: Connector endpoints must run from OutPort to InPort
The corrected metamodel narrows the source to OutPort and target to InPort. The corrected model also points from OrGate.open to AndGate.open. The same EVL check now passes.
context Connector {
constraint OutputToInput {
check: self.source.isTypeOf(OutPort) and self.target.isTypeOf(InPort)
message: "Connector endpoints must run from OutPort to InPort"
}
}
More files: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" type="DIGITAL"/>
<inPort name="window" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" type="DIGITAL"/>
<outPort name="sound" type="DIGITAL"/>
</component>
<component name="Siren">
<inPort name="sound" type="DIGITAL"/>
</component>
<connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
<connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
@namespace(uri="components", prefix="components")
package components;
enum PortType {
DIGITAL;
ANALOG;
}
class Architecture {
attr String[1] name;
val Component[*]#architecture components;
val Connector[*] connectors;
}
class Component {
attr String[1] name;
ref Architecture#components architecture;
val Port[*] ports;
}
abstract class Port {
attr String[1] name;
attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
All constraints are satisfied.
These are two kinds of evidence. The metamodel restricts reference types; EVL checks a rule that can also be applied to the broad draft. A more realistic review would add uniqueness, signal-type compatibility and connection completeness checks from Module 5.
Validate generated claims
An assistant might describe the alarm as having three connectors. The model has two. This illustrative claim becomes a constraint so the disagreement is observable rather than a matter of confidence in prose.
context Architecture {
constraint ThreeConnectorsClaim {
check: self.connectors.size() = 3
message: "Draft claim said three connectors; model has " + self.connectors.size()
}
}
More files: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" type="DIGITAL"/>
<inPort name="window" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" type="DIGITAL"/>
<outPort name="sound" type="DIGITAL"/>
</component>
<component name="Siren">
<inPort name="sound" type="DIGITAL"/>
</component>
<connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
<connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
@namespace(uri="components", prefix="components")
package components;
enum PortType {
DIGITAL;
ANALOG;
}
class Architecture {
attr String[1] name;
val Component[*]#architecture components;
val Connector[*] connectors;
}
class Component {
attr String[1] name;
ref Architecture#components architecture;
val Port[*] ports;
}
abstract class Port {
attr String[1] name;
attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
Error [ThreeConnectorsClaim]: Draft claim said three connectors; model has 2
Translate important claims into tests or queries: element counts, type membership, reachability, uniqueness, and generated-file contents. Keep the claim close to its check. A passing check supports only the property it tests; it is not a general proof that the entire design is correct.
Generate prompts from models
The flow can also run from a model to a prompt. EGX makes one review prompt per component, and EGL fills in the actual port names. These are generated text files for a person or a later tool to use; running this example does not send them anywhere.
rule Component2Prompt
transform c : Component {
template: "template.egl"
target: c.name + "-review.txt"
}
[%
var prompt = "Review component " + c.name + ".\nInputs:";
for (p in c.ports.select(p | p.isTypeOf(InPort))) { prompt += " " + p.name; }
prompt += "\nOutputs:";
for (p in c.ports.select(p | p.isTypeOf(OutPort))) { prompt += " " + p.name; }
prompt += "\nCheck missing inputs and type mismatches against the model; do not invent ports.\n";
%][%=prompt%]
More files: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" type="DIGITAL"/>
<inPort name="window" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" type="DIGITAL"/>
<outPort name="sound" type="DIGITAL"/>
</component>
<component name="Siren">
<inPort name="sound" type="DIGITAL"/>
</component>
<connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
<connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
@namespace(uri="components", prefix="components")
package components;
enum PortType {
DIGITAL;
ANALOG;
}
class Architecture {
attr String[1] name;
val Component[*]#architecture components;
val Connector[*] connectors;
}
class Component {
attr String[1] name;
ref Architecture#components architecture;
val Port[*] ports;
}
abstract class Port {
attr String[1] name;
attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
--- AndGate-review.txt ---
Review component AndGate.
Inputs: open armed
Outputs: sound
Check missing inputs and type mismatches against the model; do not invent ports.
--- OrGate-review.txt ---
Review component OrGate.
Inputs: door window
Outputs: open
Check missing inputs and type mismatches against the model; do not invent ports.
--- Siren-review.txt ---
Review component Siren.
Inputs: sound
Outputs:
Check missing inputs and type mismatches against the model; do not invent ports.
The prompt asks the reviewer to avoid inventing ports and to compare the answer with the source model. If an LLM later produces an answer, save the answer as a separate draft and run the appropriate Epsilon checks again. Do not silently copy a response into a trusted model.
Use an evidence loop
Start with a domain requirement, request or write a draft, run a parser and checks, inspect their output, then revise the draft. Record both the input and the exact check result. For generated programs, run them on at least a normal, an edge and an invalid model. For generated text, inspect the emitted files as well as whether the generator completed.
The tools test different things: Emfatic checks metamodel syntax, Flexmi loads instances, EVL checks domain constraints, ETL checks a mapping through its target, and EGL/EGX exposes generated text. Human review remains necessary for requirements that have not yet been formalised.
Exercises
Find the gap. Why can the first draft run even though its first connector is reversed?
Show solution
Both endpoint references are typed as the broad Port class, so either concrete port subtype is accepted. Successful execution does not imply that the intended output-to-input rule holds.
Separate evidence. What does a successful Emfatic parse establish? What does the EVL direction constraint establish?
Show solution
Parsing establishes that the metamodel syntax can be processed. The EVL constraint checks the direction of each connector in the loaded model. Neither alone establishes all domain requirements.
Review a claim. A generated paragraph says there are three components and three connectors. Which part is true for the corrected alarm?
Show solution
There are three components, OrGate, AndGate and Siren, but only two connectors. Check counts against the model, not the paragraph.
Check the draft’s direction. Add an EVL constraint to reject any connector whose source is not an OutPort or target is not an InPort.
Show solution
The illustrative reversed draft fails the constraint:
context Connector {
constraint Directed {
check: self.source.isTypeOf(OutPort) and self.target.isTypeOf(InPort)
message: "Reversed or incorrectly typed connector"
}
}
More files: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" type="DIGITAL"/>
<inPort name="window" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" type="DIGITAL"/>
<outPort name="sound" type="DIGITAL"/>
</component>
<component name="Siren">
<inPort name="sound" type="DIGITAL"/>
</component>
<connector source="Alarm.AndGate.open" target="Alarm.OrGate.open"/>
<connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
@namespace(uri="components", prefix="components")
package components;
enum PortType {
DIGITAL;
ANALOG;
}
class Architecture {
attr String[1] name;
val Component[*]#architecture components;
val Connector[*] connectors;
}
class Component {
attr String[1] name;
ref Architecture#components architecture;
val Port[*] ports;
}
abstract class Port {
attr String[1] name;
attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}
class Connector {
ref Port source;
ref Port target;
}
Error [Directed]: Reversed or incorrectly typed connector
Verify a count. Replace the false three-connector claim with a check that the corrected alarm has exactly two connectors.
Show solution
The corrected model passes:
context Architecture {
constraint TwoConnectors {
check: self.connectors.size() = 2
message: "Expected two connectors"
}
}
More files: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" type="DIGITAL"/>
<inPort name="window" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" type="DIGITAL"/>
<outPort name="sound" type="DIGITAL"/>
</component>
<component name="Siren">
<inPort name="sound" type="DIGITAL"/>
</component>
<connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
<connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
@namespace(uri="components", prefix="components")
package components;
enum PortType {
DIGITAL;
ANALOG;
}
class Architecture {
attr String[1] name;
val Component[*]#architecture components;
val Connector[*] connectors;
}
class Component {
attr String[1] name;
ref Architecture#components architecture;
val Port[*] ports;
}
abstract class Port {
attr String[1] name;
attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
All constraints are satisfied.
Improve a prompt. Add each port’s signal type and concrete port class to the generated review prompt.
Show solution
The template iterates the model’s ports and emits their declared values:
rule Component2Prompt
transform c : Component {
template: "template.egl"
target: c.name + "-typed-review.txt"
}
Component [%=c.name%]
[% for (p in c.ports) { %][%=p.name%]: [%=p.type%] ([%=p.eClass().name%])
[% } %]Verify each connection against these declared ports. Report uncertainty explicitly.
More files: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" type="DIGITAL"/>
<inPort name="window" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" type="DIGITAL"/>
<outPort name="sound" type="DIGITAL"/>
</component>
<component name="Siren">
<inPort name="sound" type="DIGITAL"/>
</component>
<connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
<connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
@namespace(uri="components", prefix="components")
package components;
enum PortType {
DIGITAL;
ANALOG;
}
class Architecture {
attr String[1] name;
val Component[*]#architecture components;
val Connector[*] connectors;
}
class Component {
attr String[1] name;
ref Architecture#components architecture;
val Port[*] ports;
}
abstract class Port {
attr String[1] name;
attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
--- AndGate-typed-review.txt ---
Component AndGate
open: DIGITAL (InPort)
armed: DIGITAL (InPort)
sound: DIGITAL (OutPort)
Verify each connection against these declared ports. Report uncertainty explicitly.
--- OrGate-typed-review.txt ---
Component OrGate
door: DIGITAL (InPort)
window: DIGITAL (InPort)
open: DIGITAL (OutPort)
Verify each connection against these declared ports. Report uncertainty explicitly.
--- Siren-typed-review.txt ---
Component Siren
sound: DIGITAL (InPort)
Verify each connection against these declared ports. Report uncertainty explicitly.
Self-check quiz
Port endpoint type allow?Port.Further reading
- Dimitris Kolovos, Metamodelling with ChatGPT, Episode 2, for the experiment adapted here.
- The EVL reference and EGX reference describe the two executable parts of this workflow.