LPG Modeler
Design

Why the file is the source of truth

The decisions behind LPG Modeler, including the ones that were rejected. If you are considering contributing a generator, or wondering why the metamodel stops where it does, start here.

Three packages: core holds the pipeline, cli and vscode both depend on core, and core never imports the VS Code API.
Three packages. The durable asset is the intermediate representation and the generators that consume it; the surface syntax stays swappable.

The canonical artifact is text

The model is a hand-editable YAML file holding semantics only. Diagram coordinates live in a separate sidecar, so rearranging a diagram never dirties the semantic diff, and layout is keyed by stable element id rather than by type name — so renaming a type preserves its position on every diagram.

Owning no parser is a deliberate trade. YAML validated by a contributed JSON Schema means completion, hover and structural errors come from VS Code's existing tooling at no cost. A concise custom DSL with a real language server remains a later option rather than a prerequisite, because the durable asset was never the syntax.

The canvas is a companion, not a replacement editor

Registering the canvas as a CustomTextEditorProvider was rejected: it would have become the default editor for model files and hidden the YAML, forfeiting exactly the schema-driven completion that motivated choosing YAML in the first place. Instead it opens beside the file, in the manner of Markdown preview.

Canvas edits reach the file as workspace edits, so VS Code owns undo and dirty state. The webview holds no model state at all: it posts a named intent, the extension host turns that into edits, and a fresh projection comes back — so nothing on screen can diverge from the file.

The canvas posts an intent to the extension host, which splices the YAML file, which is re-resolved into a fresh projection sent back to the canvas.
The intent translation lives in the extension package but imports no editor API, so the whole authoring surface is tested without a running VS Code.

Targeted edits, never re-serialization

Every canvas action becomes a set of targeted text splices computed from the YAML syntax tree. This is not an optimization — it is the difference between a reviewable diff and an unreviewable one.

Document.toString() normalizes flow-collection padding across the whole file, so re-serializing would turn a one-property change into a whole-file diff. Splicing keeps the change minimal: renaming a type alters exactly the lines that name it. A block's extent is found by indentation rather than by node range, because a YAML node's own range can run past its block into whatever follows.

Two consequences fall out of this design. Deleting a node type also deletes the edge types that reference it, because leaving the reference behind would produce a model that cannot resolve. And renaming a type first records its previous IRI, so the ontology can assert equivalence to the identity consumers already have.

The package boundary

core holds parsing, the intermediate representation, validation and every generator. cli wraps it for continuous integration. vscode adds only webview and diagnostics plumbing.

core must never import vscode, enforced by an ESLint rule and by a test that scans the source. That single rule is what keeps generator tests runnable in plain Node with no editor harness — and it is what makes gating a pull request on schema validity possible at all.

Views cap what any one diagram shows

A view names a subset of types plus an optional neighbourhood expansion, and layout nests under the view. One model can therefore carry an overview diagram beside several focused ones.

The alternative — welding diagram scope to module boundaries — was rejected because it would make people split modules for presentation reasons. Views drift as a model grows, so validation reports types that appear in no view.

This also sets the rendering budget. The canvas is built on React Flow with ELK for automatic layout; React Flow is DOM-based and degrades past a few hundred nodes, which is acceptable precisely because views cap how much any one diagram shows.

Three files: the model holds semantics, a views sidecar holds diagram scope, and a layout sidecar holds coordinates keyed by stable element ids.
Sidecar entries nest under a named view, not under the model as a whole.

Modularity means two separate things

Models compose across files, and generators sit behind a registry. Only the first of those is a public feature today.

Model composition is a metamodel feature and cannot be retrofitted once models exist in the wild, so it landed first. A public plugin API is deliberately deferred until three real generators have shown where the seam actually falls — the capability matrix is what that API will eventually expose.

Verification

Every generator has golden-file tests for output stability. The Ladybug target additionally executes its generated DDL against an in-process LadybugDB instance, then asserts that the declared constraints actually reject invalid data.

Real execution is affordable here because the database is embedded — no container is required. Neo4j and Memgraph have no embedded mode, so they keep golden coverage with containerised tests gated behind an opt-in flag. A golden file alone only proves that output has not changed, not that it is valid: the // versus -- comment syntax bug in the Ladybug output was caught by execution and would have been invisible to a golden test.

What is deliberately out of scope

One amendment worth recording

The original plan deferred interactive editing to a second release and shipped a read-only canvas first. That was amended. Building the compiler first would have left the tool unusable for its stated purpose until v2, and the intermediate representation is exercised by every canvas action anyway — so real use validates the metamodel in a way that tests alone cannot.

Where to go next