Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .deslop.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@

[defaults]
# Generator OUTPUT, not authored code. Hidden from the report, still analysed,
# so a cluster with one hand-written member survives.
# so a cluster with one hand-written member survives. Every directory here is
# one `make golden` / `make example` rewrites and AGENTS.md forbids editing by
# hand: its shape is decided by a template, and the template IS analysed.
report_hide = [
"examples/storefront/lib/**",
"examples/dmx_sqlite_example/lib/**",
"examples/dmx_openapi_example/lib/**",
"src/dmx/tests/golden/**",
"src/dmx/tests/typediagram/golden/lib/**",
]

[threshold]
max_duplication_percent = 7.0
# 0.5.1 is not bit-deterministic: three runs over an unchanged tree measured
# 6.36%, 6.59% and 6.60%. The budget carries headroom over the worst of those,
# so ratchet DOWN in steps that leave some — a gate that flakes red is a gate
# people learn to re-run.
max_duplication_percent = 6.8
21 changes: 17 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ EXAMPLE_DIR := examples/storefront
CORPUS_DIR := $(TARGET_DIR)/corpus
DMX_PACKAGE_DIR := src/dart_packages/dmx
GOLDEN_DIR := $(CRATE_DIR)/tests/golden
TD_GOLDEN_DIR := $(CRATE_DIR)/tests/typediagram/golden

# Every Dart directory a human writes. Deliberately enumerated rather than
# globbed: what is NOT here is dmx output, and formatting output rewrites the
Expand Down Expand Up @@ -126,15 +127,16 @@ lint: ## Clippy with warnings denied (read-only — never formats)
fmt: ## Format code in-place. Pass CHECK=1 for read-only check (CI use)
cargo fmt $(CRATE) --all$(if $(CHECK), --check,)
@# Only HAND-WRITTEN Dart is formatted. `examples/storefront/lib`,
@# `examples/dmx_sqlite_example/lib` and $(GOLDEN_DIR) hold dmx OUTPUT —
@# `examples/dmx_sqlite_example/lib`, $(GOLDEN_DIR) and
@# $(TD_GOLDEN_DIR)/lib hold dmx OUTPUT —
@# formatting them would rewrite the very bytes the golden tests assert on.
@# `--output none` matters: plain `dart format --set-exit-if-changed` still
@# REWRITES the files it checks, which is not a check.
dart format --language-version=$(DART_LANGUAGE_VERSION)$(if $(CHECK), --output none --set-exit-if-changed,) $(DART_HAND_WRITTEN)

clean: ## Remove Rust and Dart build artifacts
cargo clean $(CRATE)
$(RM) $(EXAMPLE_DIR)/.dart_tool $(CORPUS_DIR) $(WEBSITE_DIR)/dist $(WEBSITE_DIR)/pkg lcov.info
$(RM) $(EXAMPLE_DIR)/.dart_tool $(TD_GOLDEN_DIR)/.dart_tool $(CORPUS_DIR) $(WEBSITE_DIR)/dist $(WEBSITE_DIR)/pkg lcov.info
@# One per component, plus the raw hit data the two of them are formatted from.
$(RM) $(DMX_PACKAGE_DIR)/lcov.info $(DMX_PACKAGE_DIR)/.coverage \
$(EXTENSION_DIR)/lcov.info $(WEBSITE_DIR)/lcov.info $(WEBSITE_DIR)/.coverage
Expand Down Expand Up @@ -308,8 +310,12 @@ dart-package-publish: dart-package ## Prove the pub archive is publishable (need
@# so this passes only from a clean checkout — which is what a tag is.
cd $(DMX_PACKAGE_DIR) && dart pub publish --dry-run

example: ## Generate the example, analyze it, run its checks
cargo run $(CRATE) --quiet -- build $(EXAMPLE_DIR)/lib --insert-regions
example: ## Generate the example — annotated Dart and its typeDiagram definitions — analyze it, run its checks
@# One invocation for both backends. Annotated Dart is generated INTO, and a
@# `.td` definition resolves its outputs against the package it belongs to
@# [typediagram.output] — so neither depends on where this runs from, unlike
@# the macro-worker examples below, whose workers are found from the cwd.
cargo run $(CRATE) --quiet -- build $(EXAMPLE_DIR)/lib $(EXAMPLE_DIR)/models --insert-regions
cd $(EXAMPLE_DIR) && dart pub get && dart analyze --fatal-infos && dart test

EXAMPLE run-example: example
Expand Down Expand Up @@ -459,3 +465,10 @@ corpus: ## Generate every golden sample and prove it is valid Dart
@printf 'name: dmx_corpus\nenvironment:\n sdk: ^3.0.0\ndependencies:\n dmx: ^0.3.0\n' > $(CORPUS_DIR)/pubspec.yaml
cargo run $(CRATE) --quiet -- build $(CORPUS_DIR)/lib --insert-regions
cd $(CORPUS_DIR) && dart pub get && dart analyze --fatal-infos
@# The typeDiagram corpus is generated the other way round: no annotated
@# Dart at all, just `tests/typediagram/corpus/*.td` rendered through the
@# canonical model template dmx ships [typediagram.canonical]. `cargo test
@# --test typediagram_golden` proves the committed files are what the binary
@# writes; this proves they are Dart the analyzer accepts, which is the
@# half a byte comparison cannot do.
cd $(TD_GOLDEN_DIR) && dart pub get && dart analyze --fatal-infos
88 changes: 83 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,77 @@ built-ins use. Two worked examples do exactly that: one reads a live
[SQLite database](examples/dmx_sqlite_example/README.md), one reads an
[OpenAPI document](examples/dmx_openapi_example/README.md).

**Models defined by a diagram.** Some types have no Dart file to annotate yet.
Write the model once as a [typeDiagram](https://typediagram.dev/docs/)
definition and save:

```text
models/parcel.td the definition
lib/parcel.dart what dmx writes
```

```typeDiagram
type Parcel {
id: Uuid
weightG: Int
insured: Option<Decimal>
}
```

```dart
final class Parcel {
const Parcel({required this.id, required this.weightG, this.insured});

final String id;
final int weightG;
final String? insured;

@override
bool operator ==(Object other) => /* every field, collections by content */;

@override
int get hashCode => Object.hash(runtimeType, id, weightG, insured);

Parcel copyWith({String? id, int? weightG, dmx.DmxPatch<String?> insured});
}

/// JSON for [Parcel].
extension ParcelJson on Parcel {
static dmx.Result<Parcel, dmx.DecodeError> fromJson(Object? json, [String path = 'Parcel']);
Map<String, Object?> toJson();
}
```

Nothing is embedded in anything: the `.td` is what any typeDiagram tool reads,
and `lib/parcel.dart` is one complete Dart file, relative to the package the
definition belongs to. The class is an immutable value — it compares by value,
hashes consistently with that comparison, and copies — and its JSON lives on an
extension beside it rather than inside it, so the class reads as what the
diagram said and nothing else. That is the **canonical model template** dmx
ships: one template, and every model class comes out of it.

To decide the shape yourself, put `parcel.mustache` beside `parcel.td` and it
takes the canonical template's place. Any other template beside the definition
is an extra output, bound by its name — the
[shipping example](examples/storefront/models/README.md) defines four types once
and generates two different Dart files from them. dmx reads the definition
itself — no Node, no npm package, no `typediagram` executable.

The definition and its templates can also live inside one `*.dmx.md` document,
when the model, the diagram, and the prose explaining them belong on one page:

````markdown
```typeDiagram
type Parcel {
id: Uuid
}
```

```mustache {"dmx":{"output":"lib/parcel.dart"}}
{{#declarations}}final class {{name}} {}{{/declarations}}
```
````

**It never writes broken Dart.**

| Guarantee | Mechanism |
Expand All @@ -120,17 +191,24 @@ built-ins use. Two worked examples do exactly that: one reads a live
| Leaves labelled folds alone | Only the bare, unlabelled `//#region` block is machine-owned |
| Repairs a region you gutted | dmx empties the region, re-parses, and regenerates [emission.inline-backend.region-recovery] |
| Zero writes when nothing changed | Byte-compare before write [emission.inline-backend.no-op-writes] |
| Generated code obeys the house rules | No `throw`, `as`, `!` or `_$` names — asserted over the whole golden corpus |
| Generated code obeys the house rules | No `throw`, `as`, `!` or `_$` names — asserted over the whole golden corpus, and enforced on the CST for templates dmx did not write [hygiene] |
| Never overwrites a file it does not own | A generated file carries an ownership marker on its first line; anything without one is yours [dartmacros.files] |

## CLI

```
dmx build [PATHS...] [--insert-regions] [--check]
dmx watch [PATHS...]
dmx build [PATHS...] [--insert-regions] [--check]
dmx watch [PATHS...]
dmx explain FILE
```

Both default to `lib`. `watch` regenerates changed `.dart` files and debounces
save bursts. `--check` writes nothing and exits 2 on drift, for CI.
`build` and `watch` default to `lib`. Both take Dart sources, `*.td` definitions
and `*.dmx.md` documents found under the paths given, and any Markdown file named
explicitly. `watch` regenerates what changed — including when you edit a
`.mustache` file beside a definition — and debounces save bursts. `--check`
writes nothing and exits 2 on drift, for CI. `dmx explain models/parcel.td`
prints each generation group, its outputs, its dependency digests, and the exact
context its templates will see — without generating anything.

## Working on dmx

Expand Down
8 changes: 4 additions & 4 deletions coverage-thresholds.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@
"_measuring": "Every component reports LCOV, and the gate sums LH/LF per component. Field order differs between producers (Node emits LH before LF, vitest emits LF before LH) — summing each field independently is order-independent, which is why the check does that rather than parsing records.",
"components": {
"rust": {
"threshold": 90,
"threshold": 93,
"lcov": "lcov.info",
"_covers": "The dmx crate at src/dmx — the parser, context builder, renderer, validator and emitter. Everything the binary does.",
"_covers": "The dmx crate at src/dmx — the parser, context builder, renderer, validator and emitter. Everything the binary does, both typeDiagram front ends included: standalone `.td` definitions with the canonical model template or a `.mustache` beside them, and Markdown documents.",
"_produced_by": "cargo llvm-cov --workspace --all-targets",
"_measured": "91.2% when the macro catalogue landed. [COVERAGE-THRESHOLDS] requires 85 for a CLI tool, which this clears."
"_measured": "94.0% (6560/6977 lines) when the canonical model template landed, up from 93.7% when the typeDiagram Markdown macro did and 91.2% when the macro catalogue did. The threshold stays at 93 rather than tracking the measure exactly: a floor with no headroom fails CI on noise rather than on a regression. [COVERAGE-THRESHOLDS] requires 85 for a CLI tool, which this clears twice over."
},
"dart-package": {
"threshold": 13,
"lcov": "src/dart_packages/dmx/lcov.info",
"_covers": "src/dart_packages/dmx/lib — the published runtime every consumer of dmx depends on: Result/Ok/Err, the decoders, DmxPatch, the transport seam, and the macro-authoring API.",
"_produced_by": "dart test --coverage + coverage:format_coverage --report-on=lib",
"_measured": "13.1% (54/411 lines) on 2026-08-15, up from 11.4% (47/411) when the release stamper's tests started exercising Result. THE WORST NUMBER IN THE REPO AND THE ONE THAT MATTERS MOST — this package is on pub.dev. dmx.dart, support.dart and transport.dart are still almost entirely unmeasured. The runtime IS exercised end-to-end by the storefront's 179 tests and the golden corpus, which is why nothing has broken; it is not exercised by its own suite, so a refactor of it is unprotected. Ratchet this hard."
"_measured": "13.1% (54/411 lines) on 2026-08-15, up from 11.4% (47/411) when the release stamper's tests started exercising Result. THE WORST NUMBER IN THE REPO AND THE ONE THAT MATTERS MOST — this package is on pub.dev. dmx.dart, support.dart and transport.dart are still almost entirely unmeasured. The runtime IS exercised end-to-end by the storefront's suite and the golden corpus, which is why nothing has broken; it is not exercised by its own suite, so a refactor of it is unprotected. Ratchet this hard."
},
"vscode-extension": {
"threshold": 95,
Expand Down
17 changes: 14 additions & 3 deletions docs/messaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ dmx does not replace one fixed model shape with another. Teams encode their exac

Custom macros and Mustache templates are one system, not competing options. A macro can return Dart directly, and a small one usually should. It can also hand its model to a Mustache template and let dmx render it with the same engine the built-ins use, which is how a project keeps generation logic and output shape in separate files: the macro answers questions only the project can answer, and the template decides what the emitted Dart looks like. The [OpenAPI example](../examples/dmx_openapi_example/README.md) reads a published API document and renders a typed client and its models through project-owned templates.

> Save the file and keep coding. Use a built-in, change what it emits with a Mustache template, or write a macro in Dart—and render a Mustache template from inside that macro too.
Some models have no Dart file to annotate yet. Write the model once as a [typeDiagram](https://typediagram.dev/docs/) definition—`shipping.td`—and save. dmx writes one complete Dart file: immutable classes with `==`, `hashCode`, `toString`, and `copyWith`, a sealed class per union, and JSON on an extension beside each class rather than inside it. That comes from the canonical model template dmx ships—the same one every model class comes out of—and nothing is embedded in anything: the `.td` file is what any typeDiagram tool reads. Put a Mustache template beside the definition and it takes the canonical template's place; add another and you get another file. The [shipping example](../examples/storefront/models/README.md) defines four types once and generates two different Dart files from them. The definition and its templates can also live inside one `*.dmx.md` document, when the model, the diagram, and the prose explaining them belong on one page.

> Save the file and keep coding. Use a built-in, change what it emits with a Mustache template, write a macro in Dart—and render a Mustache template from inside that macro too—or define the types in a diagram and let dmx write the Dart.

## Ready-to-use copy

Expand All @@ -48,9 +50,13 @@ Open the project, edit Dart, and save. dmx updates generated code automatically,

[Try the real generator in your browser](https://dmx.dev/playground.html)—no install required.

### Models from a diagram

**Define the types once in a typeDiagram file and save; dmx writes the Dart.** Immutable classes that compare by value, with JSON beside them instead of inside them—no annotated Dart source, no `part` file, and the definition still renders as a diagram. Put a Mustache template beside it to decide the shape yourself, or put the definition and its templates in a `*.dmx.md` document to keep them on one page.

### Repository

Fast Dart code generation on every save, with no generated `part` files: built-in macros, team-owned Mustache templates, custom Dart macros, and validated inline output.
Fast Dart code generation on every save, with no generated `part` files: built-in macros, team-owned Mustache templates, custom Dart macros, models defined in a typeDiagram file, and validated inline output.

## Message order

Expand All @@ -60,7 +66,8 @@ Fast Dart code generation on every save, with no generated `part` files: built-i
4. **Useful immediately:** built-ins cover common models, unions, routes, clients, and more.
5. **The team's shape:** Mustache controls the exact generated Dart.
6. **Full custom macros:** inspect typed declaration data, read project data, and generate members or complete files—returning Dart directly, or rendering it through the same Mustache engine the built-ins use.
7. **Reliable writes:** validate complete Dart files before writing and preserve handwritten source on failure.
7. **Models with no Dart to annotate:** a typeDiagram definition file generates the `.dart` file on its own, through the canonical model template or through a Mustache template beside it—or a `*.dmx.md` document does the same on one page.
8. **Reliable writes:** validate complete Dart files before writing and preserve handwritten source on failure.

## Demo order

Expand All @@ -69,6 +76,7 @@ Fast Dart code generation on every save, with no generated `part` files: built-i
3. Rename a field, save, and show generated members update immediately.
4. Change a Mustache template and show the team's model shape appear.
5. Add a SQLite table and show a complete Dart file appear.
6. Add a field to a typeDiagram definition, save, and show both generated Dart files change together—`==`, `hashCode`, `copyWith`, and the JSON extension all move with it.

## Positioning

Expand Down Expand Up @@ -106,6 +114,9 @@ Version control is the team's choice. Commit generated files when a checkout sho
- Never present Mustache and custom Dart macros as a choice between two paths. A macro may return Dart directly *or* render a template, and the two are designed to be used together; describing templates as what you use “instead of” a macro, or a macro as what you write “when Mustache runs out”, misstates the design.
- “Commit or ignore” applies directly to complete generated files; inline output is committed with its source file.
- Never attack `build_runner`, claim it cannot watch, call dmx a drop-in Freezed replacement, or lead with compiler-architecture jargon. Lead with no generated `part` files.
- Never say dmx "runs typeDiagram", "calls the typeDiagram CLI", or "uses `typediagram --to dart`". It does none of those. dmx reads the definition itself, and the Mustache template decides every generated byte—so never imply the output shape comes from typeDiagram either. Installing dmx installs nothing else: no Node, no npm package, no `typediagram` executable.
- A template binds to the definition **immediately above it**. Never show or describe a heading, a paragraph, or another fence between them, and never suggest binding follows a heading's text or a fence's position in the document.
- An output path in a document is relative to the package the document belongs to—the nearest `pubspec.yaml`. Do not describe it as relative to the document, to the repository root, or to wherever dmx was run.

## Calls to action

Expand Down
2 changes: 1 addition & 1 deletion docs/plans/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The backlog is split by topic so each plan stays small and focused.
| [Implementation phases](implementation-phases.md) | `[phases]` | Release sequence and phase exit criteria |
| [Macro catalogue](macro-catalogue.md) | `[catalogue]` | Built-in and user-defined macro backlog |
| [Worked examples](worked-examples.md) | `[corpus]` | Storefront and golden-corpus coverage |
| [typeDiagram integration](typediagram-integration.md) | `[typediagram.delivery]` | typeDiagram Markdown definitions plus Mustache templates to generated Dart |
| [typeDiagram integration](typediagram-integration.md) | `[typediagram.delivery]` | typeDiagram Markdown definitions plus Mustache templates to generated Dart — **delivered**; the normative rules are [typediagram] in `SPEC.md`, and what is left is [typediagram.delivery.next] |
| [Semantic front end and static metaprogramming](semantic-metaprogramming.md) | `[semantic-expansion]` | Scope resolution, type inference, typed expansion, and elaborated type-system extensions |

Every section in these files has a unique, hierarchical, non-numeric identifier shared with `SPEC.md`. Adding a plan file does not create a second identifier namespace.
Loading
Loading