domaintrail 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.ja.md +303 -0
- package/CHANGELOG.md +304 -0
- package/README.md +65 -181
- package/dist/dt.js +398 -85
- package/dist/web/chunk-4bbfrcw1.css +1 -0
- package/dist/web/{chunk-q5qj2my7.js → chunk-64rawkq2.js} +108 -108
- package/dist/web/index.html +1 -1
- package/package.json +4 -2
- package/dist/web/chunk-fkmsbzfr.css +0 -1
package/dist/dt.js
CHANGED
|
@@ -11477,6 +11477,95 @@ var init_elk = __esm(() => {
|
|
|
11477
11477
|
};
|
|
11478
11478
|
});
|
|
11479
11479
|
|
|
11480
|
+
// src/server/git.ts
|
|
11481
|
+
import { execFile, spawn } from "node:child_process";
|
|
11482
|
+
import * as path6 from "node:path";
|
|
11483
|
+
function git(cwd, args) {
|
|
11484
|
+
return new Promise((resolve, reject) => {
|
|
11485
|
+
execFile("git", args, { cwd, maxBuffer: MAX_BUFFER, encoding: "utf8" }, (err, stdout) => err ? reject(err) : resolve(stdout));
|
|
11486
|
+
});
|
|
11487
|
+
}
|
|
11488
|
+
async function gitDirs(root) {
|
|
11489
|
+
try {
|
|
11490
|
+
const [gitDir, commonDir] = (await git(root, ["rev-parse", "--absolute-git-dir", "--git-common-dir"])).trim().split(`
|
|
11491
|
+
`);
|
|
11492
|
+
return [...new Set([gitDir, path6.resolve(root, commonDir)])];
|
|
11493
|
+
} catch {
|
|
11494
|
+
return;
|
|
11495
|
+
}
|
|
11496
|
+
}
|
|
11497
|
+
async function resolveCommit(root, ref) {
|
|
11498
|
+
let hash;
|
|
11499
|
+
try {
|
|
11500
|
+
hash = (await git(root, ["rev-parse", "--verify", "--quiet", "--end-of-options", `${ref}^{commit}`])).trim();
|
|
11501
|
+
} catch {
|
|
11502
|
+
throw new UnknownRevisionError(`Unknown revision '${ref}'`);
|
|
11503
|
+
}
|
|
11504
|
+
const [subject = "", date = ""] = (await git(root, ["log", "-1", "--format=%s%x00%cI", hash])).trimEnd().split("\x00");
|
|
11505
|
+
return { hash, subject, date };
|
|
11506
|
+
}
|
|
11507
|
+
async function listRefs(root, limit = 30) {
|
|
11508
|
+
const lines = (out) => out.split(`
|
|
11509
|
+
`).filter(Boolean);
|
|
11510
|
+
const refs = (pattern) => git(root, ["for-each-ref", "--format=%(refname:short)", pattern]).then(lines, () => []);
|
|
11511
|
+
const [branches, tags, log] = await Promise.all([
|
|
11512
|
+
refs("refs/heads"),
|
|
11513
|
+
refs("refs/tags"),
|
|
11514
|
+
git(root, ["log", `-${limit}`, "--format=%H%x00%h%x00%s%x00%cI", "HEAD", "--"]).then(lines, () => [])
|
|
11515
|
+
]);
|
|
11516
|
+
const commits = log.map((l) => {
|
|
11517
|
+
const [hash, short, subject, date] = l.split("\x00");
|
|
11518
|
+
return { hash, short, subject, date };
|
|
11519
|
+
});
|
|
11520
|
+
return { branches, tags, commits };
|
|
11521
|
+
}
|
|
11522
|
+
async function sourcesAt(root, commit, sources) {
|
|
11523
|
+
const matches = sourceMatcher(sources);
|
|
11524
|
+
const entries = (await git(root, ["ls-tree", "-r", "-z", commit, "--"])).split("\x00").filter(Boolean).map((line) => {
|
|
11525
|
+
const tab = line.indexOf("\t");
|
|
11526
|
+
const [, type, oid] = line.slice(0, tab).split(" ");
|
|
11527
|
+
return { type, oid, path: line.slice(tab + 1) };
|
|
11528
|
+
}).filter((e) => e.type === "blob" && matches(e.path)).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
11529
|
+
if (entries.length === 0)
|
|
11530
|
+
return [];
|
|
11531
|
+
const blobs = await catBlobs(root, entries.map((e) => e.oid));
|
|
11532
|
+
return entries.map((e, i) => ({ path: e.path, text: blobs[i] }));
|
|
11533
|
+
}
|
|
11534
|
+
function catBlobs(cwd, oids) {
|
|
11535
|
+
return new Promise((resolve, reject) => {
|
|
11536
|
+
const child = spawn("git", ["cat-file", "--batch"], { cwd, stdio: ["pipe", "pipe", "ignore"] });
|
|
11537
|
+
const chunks = [];
|
|
11538
|
+
child.stdout.on("data", (c) => chunks.push(c));
|
|
11539
|
+
child.on("error", reject);
|
|
11540
|
+
child.on("close", (code) => {
|
|
11541
|
+
if (code !== 0)
|
|
11542
|
+
return reject(new Error(`git cat-file exited with ${code}`));
|
|
11543
|
+
const out = Buffer.concat(chunks);
|
|
11544
|
+
const texts = [];
|
|
11545
|
+
let at = 0;
|
|
11546
|
+
for (const oid of oids) {
|
|
11547
|
+
const eol = out.indexOf(10, at);
|
|
11548
|
+
const header = out.subarray(at, eol).toString("utf8").split(" ");
|
|
11549
|
+
if (header[0] !== oid || header[1] !== "blob")
|
|
11550
|
+
return reject(new Error(`git cat-file: unexpected object ${header.join(" ")}`));
|
|
11551
|
+
const size = Number(header[2]);
|
|
11552
|
+
texts.push(out.subarray(eol + 1, eol + 1 + size).toString("utf8"));
|
|
11553
|
+
at = eol + 1 + size + 1;
|
|
11554
|
+
}
|
|
11555
|
+
resolve(texts);
|
|
11556
|
+
});
|
|
11557
|
+
child.stdin.end(oids.map((o) => `${o}
|
|
11558
|
+
`).join(""));
|
|
11559
|
+
});
|
|
11560
|
+
}
|
|
11561
|
+
var UnknownRevisionError, MAX_BUFFER;
|
|
11562
|
+
var init_git = __esm(() => {
|
|
11563
|
+
init_config();
|
|
11564
|
+
UnknownRevisionError = class UnknownRevisionError extends Error {
|
|
11565
|
+
};
|
|
11566
|
+
MAX_BUFFER = 64 * 1024 * 1024;
|
|
11567
|
+
});
|
|
11568
|
+
|
|
11480
11569
|
// src/model/dependencies.ts
|
|
11481
11570
|
function contextDependencies(model) {
|
|
11482
11571
|
const out = [];
|
|
@@ -11598,7 +11687,7 @@ function sized(n, scale = 1) {
|
|
|
11598
11687
|
const head = Math.max(maxUnits(n.label) + 2, n.stereotype ? maxUnits(n.stereotype) : 0) * scale;
|
|
11599
11688
|
return {
|
|
11600
11689
|
...n,
|
|
11601
|
-
width: Math.max(170 * scale, Math.ceil(
|
|
11690
|
+
width: Math.max(170 * scale, Math.ceil(head * CHAR_WIDTH + 28), bodyWidth(n.lines)),
|
|
11602
11691
|
height: Math.ceil(HEADER_HEIGHT * scale) + n.lines.length * LINE_HEIGHT + (n.lines.length ? 10 : 0)
|
|
11603
11692
|
};
|
|
11604
11693
|
}
|
|
@@ -11980,7 +12069,7 @@ function eventsLabel(c, model, from, to) {
|
|
|
11980
12069
|
const each = emits.map((e) => named(short(e.event), lookupLabel(model, e.event), (l, n) => `${l} ${n}`, e.multiplicity));
|
|
11981
12070
|
return anyLang({ name: each.map((t) => t.en.name).join(", "), label: each.map((t) => t.en.label).join(", "), both: each.map((t) => t.en.both).join(", ") });
|
|
11982
12071
|
}
|
|
11983
|
-
var STRUCTURE_GRAPH_ID = "structure", stateGraphId = (aggregateId, labels = "both") => `states:${aggregateId}${labels === "both" ? "" : `:${labels}`}`, boundaryId = (aggregateId) => `${aggregateId}@boundary`, initialNodeId = (aggregateId) => `${aggregateId}@new`, stereotype = (ja, en) => perLang({ ja: `«${ja}»`, en: `«${en}»` }), STEREOTYPES, VARIANT_INDENT = " ", STRUCTURE_KINDS, HELD_KINDS, PRIMITIVE_LABELS, WORDS, lookupLabel = (model, qname) => model.elements[qname]?.label, COLUMN_GAP = 2, STRUCTURE_OVERVIEW_GRAPH_ID = "structure-overview", STRUCTURE_AGGREGATES_GRAPH_ID = "structure-aggregates", LEVEL_GRAPH_IDS, focusGraphId = (contexts, level = "detail", hidden = []) => `structure-focus:${[...contexts].sort().join("+")}${hidden.length ? `:hide=${[...hidden].sort().join("+")}` : ""}${level === "detail" ? "" : `:${level}`}`, AGGREGATE_DOC_WIDTH = 36, CONTEXT_DOC_WIDTH = 60, NO_LINE_START, STUB_STEREOTYPES;
|
|
12072
|
+
var STRUCTURE_GRAPH_ID = "structure", stateGraphId = (aggregateId, labels = "both") => `states:${aggregateId}${labels === "both" ? "" : `:${labels}`}`, boundaryId = (aggregateId) => `${aggregateId}@boundary`, initialNodeId = (aggregateId) => `${aggregateId}@new`, stereotype = (ja, en) => perLang({ ja: `«${ja}»`, en: `«${en}»` }), STEREOTYPES, VARIANT_INDENT = " ", STRUCTURE_KINDS, HELD_KINDS, PRIMITIVE_LABELS, WORDS, lookupLabel = (model, qname) => model.elements[qname]?.label, COLUMN_GAP = 2, bodyWidth = (lines) => Math.ceil(bodyUnits(lines) * CHAR_WIDTH + 28), STRUCTURE_OVERVIEW_GRAPH_ID = "structure-overview", STRUCTURE_AGGREGATES_GRAPH_ID = "structure-aggregates", LEVEL_GRAPH_IDS, focusGraphId = (contexts, level = "detail", hidden = []) => `structure-focus:${[...contexts].sort().join("+")}${hidden.length ? `:hide=${[...hidden].sort().join("+")}` : ""}${level === "detail" ? "" : `:${level}`}`, AGGREGATE_DOC_WIDTH = 36, CONTEXT_DOC_WIDTH = 60, NO_LINE_START, STUB_STEREOTYPES;
|
|
11984
12073
|
var init_build2 = __esm(() => {
|
|
11985
12074
|
init_dependencies();
|
|
11986
12075
|
init_ir();
|
|
@@ -12119,14 +12208,67 @@ function diffModels(prev, next) {
|
|
|
12119
12208
|
diff.removed.push(id);
|
|
12120
12209
|
return diff;
|
|
12121
12210
|
}
|
|
12211
|
+
function markLineChanges(graph, base) {
|
|
12212
|
+
if (!base)
|
|
12213
|
+
return graph;
|
|
12214
|
+
const before = new Map(base.nodes.map((n) => [n.id, n]));
|
|
12215
|
+
return { ...graph, nodes: graph.nodes.map((n) => markNode(n, before.get(n.id))) };
|
|
12216
|
+
}
|
|
12217
|
+
function markNode(n, base) {
|
|
12218
|
+
if (!base || base.kind !== n.kind || !n.lines.length && !base.lines.length)
|
|
12219
|
+
return n;
|
|
12220
|
+
const keys = lineKeys(n.lines);
|
|
12221
|
+
const baseKeys = lineKeys(base.lines);
|
|
12222
|
+
const baseByKey = new Map(baseKeys.map((k, i) => [k, base.lines[i]]));
|
|
12223
|
+
const present = new Set(keys);
|
|
12224
|
+
const out = [];
|
|
12225
|
+
let marked = false;
|
|
12226
|
+
const removedAfter = new Map;
|
|
12227
|
+
let previous;
|
|
12228
|
+
baseKeys.forEach((k, i) => {
|
|
12229
|
+
if (present.has(k))
|
|
12230
|
+
previous = k;
|
|
12231
|
+
else
|
|
12232
|
+
removedAfter.set(previous, [...removedAfter.get(previous) ?? [], { ...base.lines[i], change: "removed" }]);
|
|
12233
|
+
});
|
|
12234
|
+
out.push(...removedAfter.get(undefined) ?? []);
|
|
12235
|
+
n.lines.forEach((line, i) => {
|
|
12236
|
+
const old = baseByKey.get(keys[i]);
|
|
12237
|
+
const change = !old ? "added" : JSON.stringify(old) !== JSON.stringify(line) ? "changed" : undefined;
|
|
12238
|
+
if (change)
|
|
12239
|
+
marked = true;
|
|
12240
|
+
out.push(change ? { ...line, change } : line);
|
|
12241
|
+
out.push(...removedAfter.get(keys[i]) ?? []);
|
|
12242
|
+
});
|
|
12243
|
+
const removed = out.length - n.lines.length;
|
|
12244
|
+
if (!marked && !removed)
|
|
12245
|
+
return n;
|
|
12246
|
+
return {
|
|
12247
|
+
...n,
|
|
12248
|
+
lines: out,
|
|
12249
|
+
width: Math.max(n.width, bodyWidth(out)),
|
|
12250
|
+
height: n.height + removed * LINE_HEIGHT + (n.lines.length === 0 ? 10 : 0)
|
|
12251
|
+
};
|
|
12252
|
+
}
|
|
12253
|
+
function lineKeys(lines) {
|
|
12254
|
+
const seen = new Map;
|
|
12255
|
+
return lines.map((l) => {
|
|
12256
|
+
const k = l.key.en.name;
|
|
12257
|
+
const i = seen.get(k) ?? 0;
|
|
12258
|
+
seen.set(k, i + 1);
|
|
12259
|
+
return `${k}#${i}`;
|
|
12260
|
+
});
|
|
12261
|
+
}
|
|
12122
12262
|
var EMPTY_DIFF;
|
|
12123
12263
|
var init_diff = __esm(() => {
|
|
12264
|
+
init_build2();
|
|
12265
|
+
init_ir();
|
|
12124
12266
|
EMPTY_DIFF = { added: [], changed: [], removed: [] };
|
|
12125
12267
|
});
|
|
12126
12268
|
|
|
12127
12269
|
// src/server/session.ts
|
|
12128
12270
|
import { lstat, mkdir, readFile as readFile4, rename, writeFile } from "node:fs/promises";
|
|
12129
|
-
import * as
|
|
12271
|
+
import * as path7 from "node:path";
|
|
12130
12272
|
function focusableContexts(model) {
|
|
12131
12273
|
const supporting = supportingContexts(model);
|
|
12132
12274
|
const drawn = new Set(["aggregate", "entity", "valueObject", "union"]);
|
|
@@ -12141,19 +12283,26 @@ class ViewSession {
|
|
|
12141
12283
|
version = 0;
|
|
12142
12284
|
current;
|
|
12143
12285
|
shownModel;
|
|
12286
|
+
previousView = null;
|
|
12144
12287
|
shownFingerprint = "";
|
|
12145
12288
|
modelVersion = 0;
|
|
12289
|
+
previousDiff = EMPTY_DIFF;
|
|
12146
12290
|
lastDiff = EMPTY_DIFF;
|
|
12291
|
+
baselineSpec;
|
|
12292
|
+
gitBase;
|
|
12293
|
+
comparedKey = "";
|
|
12147
12294
|
layouts = new Map;
|
|
12295
|
+
baseLayouts = new Map;
|
|
12148
12296
|
focusLayouts = new Map;
|
|
12149
12297
|
layoutsLoaded = false;
|
|
12150
12298
|
queue = Promise.resolve();
|
|
12151
12299
|
warnedUnsafeLayoutDir = false;
|
|
12152
|
-
constructor(root, engine, sources = DEFAULT_SOURCES, respectGitignore = true) {
|
|
12300
|
+
constructor(root, engine, sources = DEFAULT_SOURCES, respectGitignore = true, baseRef) {
|
|
12153
12301
|
this.root = root;
|
|
12154
12302
|
this.engine = engine;
|
|
12155
12303
|
this.sources = sources;
|
|
12156
12304
|
this.respectGitignore = respectGitignore;
|
|
12305
|
+
this.baselineSpec = baseRef ? { kind: "git", ref: baseRef } : { kind: "previous" };
|
|
12157
12306
|
this.current = this.payload({
|
|
12158
12307
|
files: [],
|
|
12159
12308
|
sources: [],
|
|
@@ -12174,12 +12323,78 @@ class ViewSession {
|
|
|
12174
12323
|
return this.current;
|
|
12175
12324
|
}
|
|
12176
12325
|
refresh() {
|
|
12177
|
-
|
|
12326
|
+
return this.serialized(() => this.doRefresh());
|
|
12327
|
+
}
|
|
12328
|
+
setBaseline(spec) {
|
|
12329
|
+
return this.serialized(async () => {
|
|
12330
|
+
if (spec.kind === "git")
|
|
12331
|
+
await resolveCommit(this.root, spec.ref);
|
|
12332
|
+
this.baselineSpec = spec;
|
|
12333
|
+
this.gitBase = undefined;
|
|
12334
|
+
return this.doRefresh();
|
|
12335
|
+
});
|
|
12336
|
+
}
|
|
12337
|
+
serialized(fn) {
|
|
12338
|
+
const run = this.queue.then(fn);
|
|
12178
12339
|
this.queue = run.catch(() => {
|
|
12179
12340
|
return;
|
|
12180
12341
|
});
|
|
12181
12342
|
return run;
|
|
12182
12343
|
}
|
|
12344
|
+
async syncGitBase(ref) {
|
|
12345
|
+
let commit;
|
|
12346
|
+
try {
|
|
12347
|
+
commit = await resolveCommit(this.root, ref);
|
|
12348
|
+
} catch (e) {
|
|
12349
|
+
return this.gitBase = { ref, view: null, files: 0, error: e instanceof Error ? e.message : String(e) };
|
|
12350
|
+
}
|
|
12351
|
+
if (this.gitBase?.ref === ref && this.gitBase.commit?.hash === commit.hash)
|
|
12352
|
+
return this.gitBase;
|
|
12353
|
+
const sources = await sourcesAt(this.root, commit.hash, this.sources);
|
|
12354
|
+
const analysis = await analyze(sources);
|
|
12355
|
+
const base = { ref, commit, view: null, files: sources.length };
|
|
12356
|
+
if (!analysis.model) {
|
|
12357
|
+
base.error = `The sources at '${ref}' do not parse`;
|
|
12358
|
+
return this.gitBase = base;
|
|
12359
|
+
}
|
|
12360
|
+
const model = analysis.model;
|
|
12361
|
+
this.baseLayouts.clear();
|
|
12362
|
+
const layout = (g) => this.layout(g, this.baseLayouts);
|
|
12363
|
+
base.model = model;
|
|
12364
|
+
base.view = {
|
|
12365
|
+
model,
|
|
12366
|
+
structure: await layout(buildStructureGraph(model)),
|
|
12367
|
+
overview: await layout(buildStructureGraph(model, { level: "overview" })),
|
|
12368
|
+
aggregates: await layout(buildStructureGraph(model, { level: "aggregates" })),
|
|
12369
|
+
contextMap: await layout(buildContextMapGraph(model)),
|
|
12370
|
+
contextMapAll: await layout(buildContextMapGraph(model, { all: true })),
|
|
12371
|
+
states: await this.stateGraphs(model, () => {
|
|
12372
|
+
return;
|
|
12373
|
+
}, layout)
|
|
12374
|
+
};
|
|
12375
|
+
return this.gitBase = base;
|
|
12376
|
+
}
|
|
12377
|
+
baselineModel() {
|
|
12378
|
+
return (this.baselineSpec.kind === "git" ? this.gitBase?.model : this.previousView?.model) ?? undefined;
|
|
12379
|
+
}
|
|
12380
|
+
baseline() {
|
|
12381
|
+
if (this.baselineSpec.kind === "previous")
|
|
12382
|
+
return { kind: "previous" };
|
|
12383
|
+
const b = this.gitBase;
|
|
12384
|
+
return { kind: "git", ref: this.baselineSpec.ref, files: b?.files ?? 0, ...b?.commit && { commit: b.commit }, ...b?.error && { error: b.error } };
|
|
12385
|
+
}
|
|
12386
|
+
async stateGraphs(model, base, layout) {
|
|
12387
|
+
const states = {};
|
|
12388
|
+
for (const el of Object.values(model.elements)) {
|
|
12389
|
+
if (el.kind !== "aggregate")
|
|
12390
|
+
continue;
|
|
12391
|
+
const byLabels = {};
|
|
12392
|
+
for (const l of TRANSITION_LABELS)
|
|
12393
|
+
byLabels[l] = await layout(markLineChanges(buildStateGraph(el, model, l), base(el.id, l)));
|
|
12394
|
+
states[el.id] = byLabels;
|
|
12395
|
+
}
|
|
12396
|
+
return states;
|
|
12397
|
+
}
|
|
12183
12398
|
async doRefresh() {
|
|
12184
12399
|
await this.loadLayouts();
|
|
12185
12400
|
const sources = await loadWorkspace(this.root, this.sources, this.respectGitignore);
|
|
@@ -12217,26 +12432,32 @@ class ViewSession {
|
|
|
12217
12432
|
const model = analysis.model;
|
|
12218
12433
|
const fp = JSON.stringify([...fingerprints(model)]);
|
|
12219
12434
|
if (fp !== this.shownFingerprint) {
|
|
12220
|
-
this.
|
|
12435
|
+
this.previousDiff = diffModels(this.shownModel, model);
|
|
12436
|
+
this.previousView = this.current.model ? pickBaseView(this.current) : null;
|
|
12221
12437
|
this.shownModel = model;
|
|
12222
12438
|
this.shownFingerprint = fp;
|
|
12439
|
+
}
|
|
12440
|
+
const spec = this.baselineSpec;
|
|
12441
|
+
const gitBase = spec.kind === "git" ? await this.syncGitBase(spec.ref) : undefined;
|
|
12442
|
+
const baseModel = this.baselineModel();
|
|
12443
|
+
this.lastDiff = gitBase ? gitBase.model ? diffModels(gitBase.model, model) : EMPTY_DIFF : this.previousDiff;
|
|
12444
|
+
const comparedKey = `${fp}|${spec.kind}|${gitBase?.commit?.hash ?? ""}`;
|
|
12445
|
+
if (comparedKey !== this.comparedKey) {
|
|
12446
|
+
this.comparedKey = comparedKey;
|
|
12223
12447
|
this.modelVersion++;
|
|
12224
12448
|
}
|
|
12225
|
-
const
|
|
12449
|
+
const layout = (g) => this.layout(g);
|
|
12450
|
+
const structure = await layout(markLineChanges(buildStructureGraph(model), baseModel && buildStructureGraph(baseModel)));
|
|
12226
12451
|
const focusContexts = focusableContexts(model);
|
|
12227
|
-
const contextMap = await
|
|
12228
|
-
const contextMapAll = await
|
|
12229
|
-
const overview = await
|
|
12230
|
-
const aggregates = await
|
|
12231
|
-
const
|
|
12232
|
-
|
|
12233
|
-
|
|
12234
|
-
|
|
12235
|
-
|
|
12236
|
-
for (const l of TRANSITION_LABELS)
|
|
12237
|
-
byLabels[l] = await this.layout(buildStateGraph(el, model, l));
|
|
12238
|
-
states[el.id] = byLabels;
|
|
12239
|
-
}
|
|
12452
|
+
const contextMap = await layout(buildContextMapGraph(model));
|
|
12453
|
+
const contextMapAll = await layout(buildContextMapGraph(model, { all: true }));
|
|
12454
|
+
const overview = await layout(buildStructureGraph(model, { level: "overview" }));
|
|
12455
|
+
const aggregates = await layout(buildStructureGraph(model, { level: "aggregates" }));
|
|
12456
|
+
const baseAggregate = (id) => baseModel?.elements[id]?.kind === "aggregate" ? baseModel.elements[id] : undefined;
|
|
12457
|
+
const states = await this.stateGraphs(model, (id, l) => {
|
|
12458
|
+
const agg = baseAggregate(id);
|
|
12459
|
+
return agg && baseModel && buildStateGraph(agg, baseModel, l);
|
|
12460
|
+
}, layout);
|
|
12240
12461
|
const live = new Set([
|
|
12241
12462
|
structure.id,
|
|
12242
12463
|
overview.id,
|
|
@@ -12287,6 +12508,8 @@ class ViewSession {
|
|
|
12287
12508
|
diagnostics: p.diagnostics,
|
|
12288
12509
|
modelVersion: this.modelVersion,
|
|
12289
12510
|
diff: this.lastDiff,
|
|
12511
|
+
baseline: this.baseline(),
|
|
12512
|
+
base: this.baselineSpec.kind === "git" ? this.gitBase?.view ?? null : this.previousView,
|
|
12290
12513
|
layoutEngine: this.engine.name
|
|
12291
12514
|
};
|
|
12292
12515
|
}
|
|
@@ -12298,7 +12521,9 @@ class ViewSession {
|
|
|
12298
12521
|
const focus = [...new Set(contexts.filter((c) => focusable.has(c)))].sort();
|
|
12299
12522
|
if (focus.length === 0)
|
|
12300
12523
|
return null;
|
|
12301
|
-
const
|
|
12524
|
+
const opts = { focus, level, hidden: [...new Set(hidden.filter((c) => focusable.has(c)))] };
|
|
12525
|
+
const baseModel = this.baselineModel();
|
|
12526
|
+
const graph = markLineChanges(buildStructureGraph(model, opts), baseModel && buildStructureGraph(baseModel, opts));
|
|
12302
12527
|
const signature = graphSignature(graph);
|
|
12303
12528
|
const cached = this.focusLayouts.get(graph.id);
|
|
12304
12529
|
if (cached && cached.signature === signature)
|
|
@@ -12310,14 +12535,14 @@ class ViewSession {
|
|
|
12310
12535
|
this.focusLayouts.delete(this.focusLayouts.keys().next().value);
|
|
12311
12536
|
return laid;
|
|
12312
12537
|
}
|
|
12313
|
-
async layout(graph) {
|
|
12314
|
-
const cached =
|
|
12538
|
+
async layout(graph, cache = this.layouts) {
|
|
12539
|
+
const cached = cache.get(graph.id);
|
|
12315
12540
|
const signature = graphSignature(graph);
|
|
12316
12541
|
if (cached && cached.signature === signature) {
|
|
12317
12542
|
return { ...graph, nodeLayout: cached.nodeLayout, edgeLayout: cached.edgeLayout, signature };
|
|
12318
12543
|
}
|
|
12319
12544
|
const laid = await this.engine.layout(graph);
|
|
12320
|
-
|
|
12545
|
+
cache.set(graph.id, laid);
|
|
12321
12546
|
return laid;
|
|
12322
12547
|
}
|
|
12323
12548
|
async loadLayouts() {
|
|
@@ -12325,7 +12550,7 @@ class ViewSession {
|
|
|
12325
12550
|
return;
|
|
12326
12551
|
this.layoutsLoaded = true;
|
|
12327
12552
|
try {
|
|
12328
|
-
const file = JSON.parse(await readFile4(
|
|
12553
|
+
const file = JSON.parse(await readFile4(path7.join(this.root, LAYOUT_FILE), "utf8"));
|
|
12329
12554
|
if (file.version !== 1 || file.engine !== this.engine.name || file.engineVersion !== this.engine.version)
|
|
12330
12555
|
return;
|
|
12331
12556
|
for (const [id, g] of Object.entries(file.graphs)) {
|
|
@@ -12338,8 +12563,8 @@ class ViewSession {
|
|
|
12338
12563
|
for (const [id, g] of [...this.layouts].sort(([a], [b]) => a.localeCompare(b))) {
|
|
12339
12564
|
file.graphs[id] = { signature: g.signature, nodeLayout: g.nodeLayout, edgeLayout: g.edgeLayout };
|
|
12340
12565
|
}
|
|
12341
|
-
const target =
|
|
12342
|
-
const dir =
|
|
12566
|
+
const target = path7.join(this.root, LAYOUT_FILE);
|
|
12567
|
+
const dir = path7.dirname(target);
|
|
12343
12568
|
const existing = await lstat(dir).catch(() => {
|
|
12344
12569
|
return;
|
|
12345
12570
|
});
|
|
@@ -12350,13 +12575,21 @@ class ViewSession {
|
|
|
12350
12575
|
return;
|
|
12351
12576
|
}
|
|
12352
12577
|
await mkdir(dir, { recursive: true });
|
|
12353
|
-
const temp =
|
|
12578
|
+
const temp = path7.join(dir, `.layout-${process.pid}-${Date.now()}.tmp`);
|
|
12354
12579
|
await writeFile(temp, `${JSON.stringify(file, null, 2)}
|
|
12355
12580
|
`, { flag: "wx" });
|
|
12356
12581
|
await rename(temp, target);
|
|
12357
12582
|
}
|
|
12358
12583
|
}
|
|
12359
|
-
var LAYOUT_FILE, FOCUS_LAYOUT_CACHE = 20
|
|
12584
|
+
var LAYOUT_FILE, FOCUS_LAYOUT_CACHE = 20, pickBaseView = ({ model, structure, overview, aggregates, contextMap, contextMapAll, states }) => ({
|
|
12585
|
+
model,
|
|
12586
|
+
structure,
|
|
12587
|
+
overview,
|
|
12588
|
+
aggregates,
|
|
12589
|
+
contextMap,
|
|
12590
|
+
contextMapAll,
|
|
12591
|
+
states
|
|
12592
|
+
});
|
|
12360
12593
|
var init_session = __esm(() => {
|
|
12361
12594
|
init_analyze();
|
|
12362
12595
|
init_config();
|
|
@@ -12366,32 +12599,33 @@ var init_session = __esm(() => {
|
|
|
12366
12599
|
init_diff();
|
|
12367
12600
|
init_ir();
|
|
12368
12601
|
init_dependencies();
|
|
12369
|
-
|
|
12602
|
+
init_git();
|
|
12603
|
+
LAYOUT_FILE = path7.join(".domaintrail", "layout.json");
|
|
12370
12604
|
});
|
|
12371
12605
|
|
|
12372
12606
|
// src/server/server.ts
|
|
12373
12607
|
import { existsSync as existsSync3 } from "node:fs";
|
|
12374
12608
|
import { readdir as readdir2, readFile as readFile5 } from "node:fs/promises";
|
|
12375
12609
|
import { createServer } from "node:http";
|
|
12376
|
-
import * as
|
|
12610
|
+
import * as path8 from "node:path";
|
|
12377
12611
|
import { fileURLToPath } from "node:url";
|
|
12378
12612
|
import { watch } from "chokidar";
|
|
12379
12613
|
import { WebSocket, WebSocketServer } from "ws";
|
|
12380
12614
|
function defaultWebDir() {
|
|
12381
|
-
const here =
|
|
12382
|
-
const bundled =
|
|
12383
|
-
return existsSync3(
|
|
12615
|
+
const here = path8.dirname(fileURLToPath(import.meta.url));
|
|
12616
|
+
const bundled = path8.join(here, "web");
|
|
12617
|
+
return existsSync3(path8.join(bundled, "index.html")) ? bundled : path8.resolve(here, "../../dist/web");
|
|
12384
12618
|
}
|
|
12385
12619
|
async function loadAssets(dir) {
|
|
12386
|
-
if (!existsSync3(
|
|
12620
|
+
if (!existsSync3(path8.join(dir, "index.html")))
|
|
12387
12621
|
throw new Error(`Live View page not found in ${dir} (run \`bun run build\`)`);
|
|
12388
12622
|
const assets = new Map;
|
|
12389
12623
|
for (const entry of await readdir2(dir, { recursive: true, withFileTypes: true })) {
|
|
12390
12624
|
if (!entry.isFile())
|
|
12391
12625
|
continue;
|
|
12392
|
-
const file =
|
|
12393
|
-
const urlPath = "/" +
|
|
12394
|
-
assets.set(urlPath, { type: CONTENT_TYPES[
|
|
12626
|
+
const file = path8.join(entry.parentPath, entry.name);
|
|
12627
|
+
const urlPath = "/" + path8.relative(dir, file).split(path8.sep).join("/");
|
|
12628
|
+
assets.set(urlPath, { type: CONTENT_TYPES[path8.extname(file)] ?? "application/octet-stream", body: await readFile5(file) });
|
|
12395
12629
|
}
|
|
12396
12630
|
assets.set("/", assets.get("/index.html"));
|
|
12397
12631
|
return assets;
|
|
@@ -12426,6 +12660,31 @@ function structureLevel(params) {
|
|
|
12426
12660
|
return level;
|
|
12427
12661
|
return params.get("overview") === "1" ? "overview" : "detail";
|
|
12428
12662
|
}
|
|
12663
|
+
function readBody(req) {
|
|
12664
|
+
return new Promise((resolve, reject) => {
|
|
12665
|
+
let body = "";
|
|
12666
|
+
req.setEncoding("utf8");
|
|
12667
|
+
req.on("data", (chunk) => {
|
|
12668
|
+
body += chunk;
|
|
12669
|
+
if (body.length > MAX_BODY)
|
|
12670
|
+
reject(new Error("Request body too large"));
|
|
12671
|
+
});
|
|
12672
|
+
req.on("end", () => resolve(body));
|
|
12673
|
+
req.on("error", reject);
|
|
12674
|
+
});
|
|
12675
|
+
}
|
|
12676
|
+
function parseBaseline(body) {
|
|
12677
|
+
try {
|
|
12678
|
+
const v = JSON.parse(body);
|
|
12679
|
+
if (typeof v !== "object" || v === null || !("kind" in v))
|
|
12680
|
+
return;
|
|
12681
|
+
if (v.kind === "previous")
|
|
12682
|
+
return { kind: "previous" };
|
|
12683
|
+
if (v.kind === "git" && "ref" in v && typeof v.ref === "string" && v.ref.trim())
|
|
12684
|
+
return { kind: "git", ref: v.ref.trim() };
|
|
12685
|
+
} catch {}
|
|
12686
|
+
return;
|
|
12687
|
+
}
|
|
12429
12688
|
function send(res, status, type, body) {
|
|
12430
12689
|
res.writeHead(status, { "content-type": type, "x-content-type-options": "nosniff" });
|
|
12431
12690
|
res.end(body);
|
|
@@ -12435,7 +12694,9 @@ async function startViewServer(options) {
|
|
|
12435
12694
|
const engine = options.engine ?? new ElkLayoutEngine;
|
|
12436
12695
|
const sources = options.sources ?? DEFAULT_SOURCES;
|
|
12437
12696
|
const respectGitignore = options.respectGitignore ?? true;
|
|
12438
|
-
|
|
12697
|
+
if (options.base)
|
|
12698
|
+
await resolveCommit(options.root, options.base);
|
|
12699
|
+
const session = new ViewSession(options.root, engine, sources, respectGitignore, options.base);
|
|
12439
12700
|
await session.refresh();
|
|
12440
12701
|
const assets = await loadAssets(options.webDir ?? defaultWebDir());
|
|
12441
12702
|
const host = options.host ?? "127.0.0.1";
|
|
@@ -12459,6 +12720,30 @@ async function startViewServer(options) {
|
|
|
12459
12720
|
return send(res, 500, "text/plain", "Layout failed");
|
|
12460
12721
|
}
|
|
12461
12722
|
}
|
|
12723
|
+
if (url.pathname === "/api/git/refs") {
|
|
12724
|
+
if (!isTrusted(req.headers, allowedHost))
|
|
12725
|
+
return send(res, 403, "text/plain", "Forbidden");
|
|
12726
|
+
return send(res, 200, "application/json", JSON.stringify(await listRefs(options.root)));
|
|
12727
|
+
}
|
|
12728
|
+
if (url.pathname === "/api/baseline") {
|
|
12729
|
+
if (!isTrusted(req.headers, allowedHost))
|
|
12730
|
+
return send(res, 403, "text/plain", "Forbidden");
|
|
12731
|
+
if (req.method !== "POST")
|
|
12732
|
+
return send(res, 405, "text/plain", "Method not allowed");
|
|
12733
|
+
const spec = parseBaseline(await readBody(req).catch(() => ""));
|
|
12734
|
+
if (!spec)
|
|
12735
|
+
return send(res, 400, "text/plain", 'Expected {"kind":"previous"} or {"kind":"git","ref":"<revision>"}');
|
|
12736
|
+
try {
|
|
12737
|
+
const payload = await session.setBaseline(spec);
|
|
12738
|
+
publish(JSON.stringify(payload));
|
|
12739
|
+
log(`baseline: ${spec.kind === "git" ? spec.ref : "previous"}`);
|
|
12740
|
+
return send(res, 200, "application/json", JSON.stringify(payload.baseline));
|
|
12741
|
+
} catch (e) {
|
|
12742
|
+
if (e instanceof UnknownRevisionError)
|
|
12743
|
+
return send(res, 400, "text/plain", e.message);
|
|
12744
|
+
return send(res, 500, "text/plain", "Comparison failed");
|
|
12745
|
+
}
|
|
12746
|
+
}
|
|
12462
12747
|
const asset = req.method === "GET" || req.method === "HEAD" ? assets.get(url.pathname) : undefined;
|
|
12463
12748
|
if (!asset)
|
|
12464
12749
|
return send(res, 404, "text/plain", "Not found");
|
|
@@ -12488,7 +12773,7 @@ Connection: close\r
|
|
|
12488
12773
|
const { port } = http.address();
|
|
12489
12774
|
let timer;
|
|
12490
12775
|
const onChange = (event, file) => {
|
|
12491
|
-
log(`${event} ${
|
|
12776
|
+
log(`${event} ${path8.relative(options.root, file)}`);
|
|
12492
12777
|
clearTimeout(timer);
|
|
12493
12778
|
timer = setTimeout(async () => {
|
|
12494
12779
|
const payload = await session.refresh();
|
|
@@ -12504,13 +12789,34 @@ Connection: close\r
|
|
|
12504
12789
|
ignored: (p, stats) => {
|
|
12505
12790
|
if (stats?.isDirectory())
|
|
12506
12791
|
return p !== scope.root && !scope.includesDirectory(p);
|
|
12507
|
-
return IGNORED_DIRS.has(
|
|
12792
|
+
return IGNORED_DIRS.has(path8.basename(p)) || !!stats?.isFile() && scope.sourcePath(p) === undefined;
|
|
12508
12793
|
}
|
|
12509
12794
|
});
|
|
12510
12795
|
for (const event of ["add", "change", "unlink"]) {
|
|
12511
12796
|
watcher.on(event, (file) => onChange(event, file));
|
|
12512
12797
|
}
|
|
12513
12798
|
const ready = new Promise((resolve) => watcher.once("ready", () => resolve()));
|
|
12799
|
+
let gitWatcher;
|
|
12800
|
+
const dirs = await gitDirs(options.root);
|
|
12801
|
+
if (dirs) {
|
|
12802
|
+
gitWatcher = watch(dirs.flatMap((d) => ["HEAD", "refs", "packed-refs"].map((f) => path8.join(d, f))), { ignoreInitial: true });
|
|
12803
|
+
let gitTimer;
|
|
12804
|
+
gitWatcher.on("all", () => {
|
|
12805
|
+
if (session.state.baseline.kind !== "git")
|
|
12806
|
+
return;
|
|
12807
|
+
clearTimeout(gitTimer);
|
|
12808
|
+
gitTimer = setTimeout(async () => {
|
|
12809
|
+
const before = session.state.baseline;
|
|
12810
|
+
const payload = await session.refresh();
|
|
12811
|
+
const after = payload.baseline;
|
|
12812
|
+
if (after.kind === "git" && before.kind === "git" && after.commit?.hash === before.commit?.hash)
|
|
12813
|
+
return;
|
|
12814
|
+
publish(JSON.stringify(payload));
|
|
12815
|
+
if (after.kind === "git")
|
|
12816
|
+
log(`baseline ${after.ref} is now ${after.commit?.hash.slice(0, 7) ?? "(unknown)"}`);
|
|
12817
|
+
}, 200);
|
|
12818
|
+
});
|
|
12819
|
+
}
|
|
12514
12820
|
return {
|
|
12515
12821
|
url: `http://${host.includes(":") ? `[${host}]` : host}:${port}/`,
|
|
12516
12822
|
session,
|
|
@@ -12518,6 +12824,7 @@ Connection: close\r
|
|
|
12518
12824
|
async stop() {
|
|
12519
12825
|
clearTimeout(timer);
|
|
12520
12826
|
await watcher.close();
|
|
12827
|
+
await gitWatcher?.close();
|
|
12521
12828
|
for (const ws of wss.clients)
|
|
12522
12829
|
ws.terminate();
|
|
12523
12830
|
wss.close();
|
|
@@ -12528,12 +12835,13 @@ Connection: close\r
|
|
|
12528
12835
|
}
|
|
12529
12836
|
};
|
|
12530
12837
|
}
|
|
12531
|
-
var LOOPBACK, CONTENT_TYPES, isStructureLevel = (s) => STRUCTURE_LEVELS.includes(s);
|
|
12838
|
+
var LOOPBACK, CONTENT_TYPES, isStructureLevel = (s) => STRUCTURE_LEVELS.includes(s), MAX_BODY = 4096;
|
|
12532
12839
|
var init_server = __esm(() => {
|
|
12533
12840
|
init_config();
|
|
12534
12841
|
init_files();
|
|
12535
12842
|
init_ir();
|
|
12536
12843
|
init_elk();
|
|
12844
|
+
init_git();
|
|
12537
12845
|
init_session();
|
|
12538
12846
|
LOOPBACK = ["127.0.0.1", "localhost", "[::1]"];
|
|
12539
12847
|
CONTENT_TYPES = {
|
|
@@ -12550,7 +12858,7 @@ var init_server = __esm(() => {
|
|
|
12550
12858
|
|
|
12551
12859
|
// src/lsp/server.ts
|
|
12552
12860
|
import { existsSync as existsSync4 } from "node:fs";
|
|
12553
|
-
import * as
|
|
12861
|
+
import * as path9 from "node:path";
|
|
12554
12862
|
import { format } from "node:util";
|
|
12555
12863
|
import {
|
|
12556
12864
|
AstUtils as AstUtils8,
|
|
@@ -12612,7 +12920,7 @@ class DomainWorkspace {
|
|
|
12612
12920
|
this.options = options;
|
|
12613
12921
|
}
|
|
12614
12922
|
async resolve(folder, warn) {
|
|
12615
|
-
this.start =
|
|
12923
|
+
this.start = path9.resolve(this.options.root ?? folder ?? process.cwd());
|
|
12616
12924
|
try {
|
|
12617
12925
|
this.config = await loadConfig({ start: this.start, configPath: this.options.configPath });
|
|
12618
12926
|
this.adoptable = !this.config.file;
|
|
@@ -12638,16 +12946,16 @@ class DomainWorkspace {
|
|
|
12638
12946
|
return true;
|
|
12639
12947
|
}
|
|
12640
12948
|
nearestConfig(file) {
|
|
12641
|
-
const rel =
|
|
12642
|
-
if (!rel || rel.startsWith("..") ||
|
|
12949
|
+
const rel = path9.relative(this.start, file);
|
|
12950
|
+
if (!rel || rel.startsWith("..") || path9.isAbsolute(rel))
|
|
12643
12951
|
return;
|
|
12644
|
-
for (let dir =
|
|
12952
|
+
for (let dir = path9.dirname(file);; dir = path9.dirname(dir)) {
|
|
12645
12953
|
for (const name of CONFIG_FILES) {
|
|
12646
|
-
const candidate =
|
|
12954
|
+
const candidate = path9.join(dir, name);
|
|
12647
12955
|
if (existsSync4(candidate))
|
|
12648
12956
|
return candidate;
|
|
12649
12957
|
}
|
|
12650
|
-
if (dir === this.start || dir ===
|
|
12958
|
+
if (dir === this.start || dir === path9.dirname(dir))
|
|
12651
12959
|
return;
|
|
12652
12960
|
}
|
|
12653
12961
|
}
|
|
@@ -12659,7 +12967,7 @@ class DomainWorkspace {
|
|
|
12659
12967
|
}
|
|
12660
12968
|
async files() {
|
|
12661
12969
|
const { root, sources, respectGitignore } = this.config;
|
|
12662
|
-
return (await findDomainFiles(root, sources, respectGitignore)).map((f) => URI2.file(
|
|
12970
|
+
return (await findDomainFiles(root, sources, respectGitignore)).map((f) => URI2.file(path9.join(root, f)));
|
|
12663
12971
|
}
|
|
12664
12972
|
}
|
|
12665
12973
|
|
|
@@ -12695,7 +13003,7 @@ class NearestConfig {
|
|
|
12695
13003
|
return;
|
|
12696
13004
|
this.warned.add(file);
|
|
12697
13005
|
const active = this.workspace.config.file ?? `${this.workspace.config.root} (no config file)`;
|
|
12698
|
-
this.warn(`${uri.fsPath} belongs to ${file}, not to the workspace of this session (${active}): its diagnostics are not computed. ` + `Open ${
|
|
13006
|
+
this.warn(`${uri.fsPath} belongs to ${file}, not to the workspace of this session (${active}): its diagnostics are not computed. ` + `Open ${path9.dirname(file)} as its own project to check it.`);
|
|
12699
13007
|
}
|
|
12700
13008
|
async reload() {
|
|
12701
13009
|
const { LangiumDocuments, DocumentBuilder, TextDocuments, WorkspaceLock } = this.shared.workspace;
|
|
@@ -12744,7 +13052,7 @@ class DiagnosticsPublisher {
|
|
|
12744
13052
|
const analysis = await analyze(sources);
|
|
12745
13053
|
const byUri = new Map([...uriOf.values()].map((uri) => [uri, []]));
|
|
12746
13054
|
for (const d of analysis.diagnostics) {
|
|
12747
|
-
const uri = uriOf.get(d.file) ?? URI2.file(
|
|
13055
|
+
const uri = uriOf.get(d.file) ?? URI2.file(path9.resolve(this.workspace.config.root, d.file)).toString();
|
|
12748
13056
|
byUri.set(uri, [...byUri.get(uri) ?? [], toLspDiagnostic(d)]);
|
|
12749
13057
|
}
|
|
12750
13058
|
for (const uri of new Set([...this.published, ...byUri.keys()])) {
|
|
@@ -13092,7 +13400,7 @@ when ${node.event.$refText} -> ${node.aggregate.$refText}.${node.command.$refTex
|
|
|
13092
13400
|
init_analyze();
|
|
13093
13401
|
init_config();
|
|
13094
13402
|
import { lstat as lstat2, mkdir as mkdir2, readFile as readFile6, readdir as readdir3, rename as rename2, rm, rmdir, stat, writeFile as writeFile2 } from "node:fs/promises";
|
|
13095
|
-
import * as
|
|
13403
|
+
import * as path10 from "node:path";
|
|
13096
13404
|
import { Command as Command2, InvalidArgumentError, Option } from "commander";
|
|
13097
13405
|
|
|
13098
13406
|
// src/core/lint-hint.ts
|
|
@@ -13565,7 +13873,7 @@ function showText(model, target) {
|
|
|
13565
13873
|
}
|
|
13566
13874
|
|
|
13567
13875
|
// docs/guide.md
|
|
13568
|
-
var guide_default = "# DomainTrail guide\n\n`dt` checks domain models written in the DomainTrail DSL (`.dt` files) and generates TypeScript types and command\ncontracts from them. The DSL describes domain **structure and contracts** only — never business logic, which is written\nby hand against the generated types.\n\nThis guide is written for people and AI agents using `dt` without other documentation. `dt guide` prints the quick\nreference below and the list of topics, `dt guide <topic>` one section (the name in the `<!-- topic: … -->` line above\nits heading, e.g. `dt guide requires`), `dt guide <rule>` one rule (`dt guide M010`), and `dt guide --all` all of it.\n\n<!-- topic: quick -->\n## Quick reference\n\n**The loop.** Edit `.dt` files → `dt fmt` → `dt check` until it exits 0 → `dt generate ts` → implement each command\nwith `decide` against its generated `<Command><Aggregate>Decision` type → type-check your code (`tsc --noEmit`) → run\nthe generated contract tests. Never edit generated files; change the `.dt` sources and generate again.\n\n**The DSL at a glance** (one file of a valid workspace):\n\n```dt\n// library/loan.dt\ncontext Library \"図書館\" { // a bounded context; \"…\" is a label (表示名)\n id LoanId: UUID // an ID type\n id BookId: UUID\n value Period { // a value object\n days: Int\n }\n\n aggregate Loan \"貸出\" {\n id: LoanId // identity (required)\n book: BookId\n owns period: Period\n renewals: Int\n note: String? // ? = optional\n invariant renewalsLimited { renewals >= 0 and renewals <= 2 } // holds in every state\n\n state Lent\n state Overdue\n state Returned {\n returnedAt: Timestamp // only in this state\n }\n\n create command lend(id: LoanId, book: BookId, period: Period) \"貸し出す\"\n -> Lent\n emits BookLent\n command renew(extension: Period)\n Lent -> Lent\n requires underLimit { renewals < 2 } else RenewalLimitReached\n changes period, renewals\n emits LoanRenewed\n command markOverdue() Lent -> Overdue\n command giveBack(at: Timestamp)\n Lent | Overdue -> Returned\n emits BookReturned,\n FeeCharged? when Overdue ->\n errors BookDamaged\n }\n\n event BookLent {\n loanId: LoanId\n book: BookId\n }\n event LoanRenewed {\n loanId: LoanId\n period: Period\n }\n event BookReturned {\n loanId: LoanId\n returnedAt: Timestamp\n }\n event FeeCharged {\n loanId: LoanId\n days: Int\n }\n error RenewalLimitReached\n error BookDamaged\n}\n```\n\n**The contract keywords:**\n\n- `owns x: T` — an entity or value held by the aggregate; `references x: XId -> X` — another aggregate, by ID only.\n- `invariant name { expr }` — holds whenever the aggregate (or, inside a `state`, that state) exists.\n- `state S { … }` — a lifecycle state with its own fields; without states, commands have no transitions.\n- `create command c(…) -> S` — makes a new aggregate; it takes the new ID as a parameter (commands are pure).\n- `A | B -> C | D` — the command runs in `A` or `B` and ends in `C` or `D`; one transition per source state.\n- `requires name { expr } else E` — the command is accepted only if; otherwise it returns `E` (the first one failing).\n- A rule over a changeable policy: an invariant over a policy kept on the aggregate, or `requires` (`dt guide policies`).\n- `changes a, b` — the fields it may change; without `changes`, no common field (a target state's new fields are set).\n- `emits X` — exactly one `X` on success; `X?` at most one, `X[1..*]` one or more; `X when A -> B` only then.\n- `errors E` — errors it may return besides those of `else`; on an error it emits no event.\n- `verified V of T for C? errors E` — evidence of a check outside the aggregate, as a parameter (`dt guide verified`).\n- `policy P when E -> A.c` — when event `E` happens, command `A.c` eventually runs (`dt guide reactions`).\n\n**The commands:**\n\n- `dt guide [topic]` — this reference, one section, or `--all`.\n- `dt fmt` — format the `.dt` files in place (`--check` for CI).\n- `dt check` — check the model; exit 0 when there are no errors (`--format json` for one JSON document).\n- `dt generate ts` — check, then write the TypeScript (and the contract tests, if configured).\n- `dt show <name>` — one element, command or state resolved, with who uses it; read it instead of the `.dt` files.\n- `dt lsp` — language server over stdio: the diagnostics of `dt check`, definitions, references, hover, rename.\n- `dt view` — the Live View in the browser; it runs until Ctrl+C, so run it in the background.\n\n**When the contract and the implementation disagree**, follow `@see` on the generated type to the declaration and\nchange the DSL, not the implementation: never emit an event or set a field only to satisfy the types.\n\n**Never cast to a domain type** (`as X`): make it with `make<Name>`, `parseDecimal` or a factory (`dt guide lint`).\n\n<!-- topic: workflow -->\n## Workflow\n\n1. Write or edit `.dt` files (see [DSL](#dsl) and [File layout](#file-layout-recommended)).\n2. Run `dt fmt` to put them in the canonical layout (see [Formatting](#formatting)).\n3. Run `dt check` and fix every reported problem until it exits 0.\n4. Run `dt generate ts` to write the TypeScript (it runs `check` first and writes nothing on errors).\n5. Implement the generated contracts in your own code — each command as a decision with `decide` (see\n [Decisions and `decide`](#decisions-and-decide)) — and type-check that code with your own TypeScript setup\n (e.g. `tsc --noEmit`). `dt` does not run the TypeScript compiler.\n6. With `generate.ts.tests` configured, run the generated contract tests against your implementation with your test\n runner (see [Generated tests](#generated-tests)).\n\n`dt view` starts the Live View, a browser page where people watch the model update as files are saved. It is a server\nthat keeps running and never exits by itself (stop it with Ctrl+C), so do not wait for it to finish: run it in the\nbackground, or use `dt check` for a result.\n\n`dt --help` and `dt <command> --help` list the options with examples.\n\n```bash\ndt fmt # format every *.dt file below the current directory in place\ndt check # every *.dt file below the current directory\ndt -r domain check # below domain/ (or as configured by a domaintrail.jsonc found there or above)\ndt generate ts # into <root>/generated/domain\ndt generate ts --out src/domain/generated\ndt view --port 8080 # http://127.0.0.1:8080, until Ctrl+C\n```\n\n<!-- topic: layout -->\n## File layout (recommended)\n\nFile names and directories carry no meaning: every matching `.dt` file belongs to the workspace, and a context may be\nsplit across any files. The recommended layout is one directory per context and one file per aggregate:\n\n```text\ncommon/context.dt shared context Common \"共通\" { ... } // a shared context has no aggregates\nsales/context.dt /// 説明 context Sales \"販売\" { terms { ... } }\nsales/order.dt context Sales { id OrderId ... aggregate Order { ... } entity OrderItem ... event OrderPlaced ... }\nsales/customer.dt context Sales { id CustomerId ... aggregate Customer { ... } }\nbilling/context.dt /// 説明 context Billing \"請求\" { terms { ... } }\nbilling/invoice.dt context Billing { id InvoiceId ... aggregate Invoice { ... } }\n```\n\n- `<context>/<aggregate>.dt` holds the aggregate and what only it uses: its ID, entities, events, errors, enums,\n primitives and values.\n- `<context>/context.dt` holds the context's name (表示名), description and `terms`, and the elements used by several of\n its aggregates or by none (field sets, for example). The other files open the context without a name: `context Sales {\n ... }`.\n- An aggregate's ID stays in the aggregate's file even when other aggregates reference it.\n- Directory and file names are kebab-case: `PurchaseOrder` → `purchase-order.dt`, `OrderFulfillment` →\n `order-fulfillment/`.\n\nThis is a convention, not a rule: `check` neither requires nor warns about any layout.\n\n<!-- topic: dsl -->\n## DSL\n\nThe examples below are the files of one valid workspace (the file name is in the first comment).\n\n<!-- topic: aggregates -->\n### Contexts and aggregates\n\n```dt\n// sales/order.dt\ncontext Sales { // a bounded context; may be split across files\n id OrderId: UUID // ID type with a base primitive type\n id OrderItemId: UUID\n enum CancelReason { Requested Other } // values separated by spaces (commas are allowed; `dt fmt` drops them)\n value Address {\n city: String\n street: String\n }\n\n aggregate Order {\n id: OrderId // identity field of its own ID type (required)\n owns items: OrderItem[0..*] // owned entity / value inside the aggregate\n references customer: CustomerId -> Customer // another aggregate, by ID\n owns shipTo: Address? // ? = optional\n note: String?\n\n invariant quantitiesPositive { // applies to every state; sees the common fields\n all(items, item => item.quantity > 0)\n }\n\n create command place(id: OrderId, customer: CustomerId)\n -> Draft\n emits OrderPlaced // factory: `-> State`, no source\n\n state Draft\n state Confirmed {\n confirmedAt: Timestamp // exists only in this state\n invariant hasItems { count(items) > 0 and count(items) <= 100 } // sees common + Confirmed fields\n }\n state Cancelled {\n reason: CancelReason\n invariant otherNeedsNote { reason == CancelReason.Other implies exists(note) }\n }\n\n command confirm(at: Timestamp)\n Draft -> Confirmed // a transition: source state(s) -> target state(s)\n requires hasItems { count(items) > 0 } else EmptyOrder // accepted only if; otherwise returns EmptyOrder\n emits OrderConfirmed // exactly one OrderConfirmed whenever it succeeds\n\n command cancel(reason: CancelReason)\n Draft | Confirmed -> Cancelled // from either\n emits OrderCancelled,\n RefundRequested when Confirmed -> // only when it comes from Confirmed\n\n command changeNote(note: String?) // one transition per input state: the output depends on the input\n Draft -> Draft\n Confirmed -> Confirmed // one transition per line (`Draft -> Draft, Confirmed -> Confirmed` is allowed too)\n changes note // the fields it may change; without `changes`, no common field\n }\n\n entity OrderItem {\n id: OrderItemId\n quantity: Quantity\n owns unitPrice: Money\n }\n\n persistent event OrderPlaced { // storing it is part of its contract\n orderId: OrderId\n }\n persistent event OrderConfirmed {\n orderId: OrderId\n confirmedAt: Timestamp\n }\n event OrderCancelled { // not persisted (default)\n orderId: OrderId\n reason: CancelReason\n }\n event RefundRequested {\n orderId: OrderId\n }\n error EmptyOrder // payload optional: error E { field: Type }\n}\n```\n\n```dt\n// sales/customer.dt\ncontext Sales {\n id CustomerId: UUID\n /// 他の顧客に使われていないことを、登録・変更のときに確かめたメールアドレス。\n verified AvailableEmail \"未使用のメールアドレス\" of EmailAddress for CustomerId? errors EmailTaken // dt guide verified\n\n aggregate Customer { // no states: commands have no transitions\n id: CustomerId\n email: EmailAddress // from the shared context Common\n active: Bool\n owns addresses: Address[0..*]\n owns shipping: Address?\n\n invariant activeHasAddress { active == true implies count(addresses) > 0 }\n invariant servedArea { active == false or any(addresses, a => a.city == \"Tokyo\" or not (a.city == \"Osaka\")) }\n invariant distinctAddresses { unique(addresses, a => a.city, a.street) } // the combination is distinct\n invariant shippingListed { exists(shipping) implies contains(addresses, shipping) }\n\n /// No other customer has the same email address: checked when one is given (AvailableEmail), kept by the store.\n invariant uniqueEmail \"メールアドレスは顧客ごとに一意\"\n\n command changeEmail(email: AvailableEmail) // the check was made before: its error is the factory's\n changes email\n }\n\n error EmailTaken {\n email: EmailAddress\n }\n}\n```\n\n- Context elements: `id`, `primitive`, `enum`, `value`, `union`, `entity`, `aggregate`, `event`, `error`, `fields`\n (see [Field sets](#field-sets)), `verified` (see [Verified types](#verified-types)) and `policy` (see\n [Reactions to events](#reactions-to-events-policies)).\n- Fields: `name: Type`, `owns name: Type` (entities and values held by the aggregate),\n `references name: IdType -> Aggregate` (other aggregates, by ID only). Entities are owned by exactly one aggregate.\n- Primitive types: `String`, `Int`, `Decimal`, `Bool`, `Timestamp`, `UUID`.\n- Multiplicity after the type: `?` (also `[0..1]`, which `dt fmt` writes as `?`), `[0..*]`, `[1..*]`, `[n..m]`.\n- Aggregate members: fields, `invariant`, `state` (with its own fields and invariants), `command`.\n- A command is written in this order: parameters and label, transitions, `requires` (see\n [Command requirements](#command-requirements)), `changes` (see [Command changes](#command-changes)), `emits` (see\n [Command events](#command-events)), `errors`.\n- Transitions: `A | B -> C | D` means \"in `A` or `B`, the command ends in `C` or `D`\". Separate several transitions by\n line breaks or commas (`dt fmt` writes one per line). A state may be the source of one transition of a command only,\n so the input state decides the possible outputs; for several possible outcomes, list them on the right: `InProgress ->\n InProgress | Completed`.\n- In an aggregate with states, a `create command` makes a new aggregate and has exactly one transition without\n sources (`-> Draft`, or `-> A | B`); every other command has one or more transitions, each with sources. Without\n states, commands have no transitions. A command that keeps the state writes it out: `Draft -> Draft`.\n- `value`, `state`, `event` and `error` bodies are optional (`state Draft`, `error EmptyOrder`).\n- Other contexts' elements are referenced with qualified names: `Sales.OrderId`, `-> Sales.Order`.\n- Comments: `// …` and `/* … */`; `///` is a description (see [Names](#names-labels-and-descriptions)).\n\n<!-- topic: primitives -->\n### Domain primitives\n\nA single value with domain meaning and constraints, wrapping a primitive type. Values combining several fields are\nvalue objects.\n\n```dt\n// sales/primitives.dt\ncontext Sales {\n primitive Quantity \"数量\": Int { value in 1..999 } // one anonymous constraint\n primitive Sku: String { // or named invariants\n invariant format \"形式\" { matches(value, \"^[A-Z]{3}-\\d{4}$\") }\n invariant maxLength { length(value) <= 8 }\n }\n id InvoiceNo: String { matches(value, \"^INV-\\d{8}$\") } // an ID is a domain primitive used as identity\n}\n```\n\n- The base is always a primitive type; a domain primitive cannot refine another one or an enum.\n- Inside the constraint only `value` (of the base type) is visible; `length(value)`, `matches(value, \"regex\")` and\n ranges `value in 1..999` (negative literals allowed) are available there.\n- Regular expressions are written as in JavaScript: backslashes are not string escapes (`\"\\d\"` means `\\d`).\n- Elsewhere a domain primitive compares with literals, with the same domain primitive and, over `Int` or `Decimal`,\n with the numbers `count(…)` and `length(…)` compute (`count(approvedBy) >= policy.requiredApprovals`); not with\n another domain primitive or a plain field or parameter of its base type (M009).\n- Only IDs can be identity fields and reference types. Domain primitives cannot be owned.\n- The generated code includes the constructor: `makeQuantity` checks the invariants and returns the first one\n violated (`makeSkuWith(checks)` when an invariant has no expression); see [Generated checks](#generated-checks).\n\n<!-- topic: unions -->\n### Unions\n\nA value whose shape depends on a kind fixed when it is made (a tagged union). Use a union for a classification that\nnever changes, and aggregate states for a lifecycle that commands move through.\n\n```dt\n// sales/payment.dt\ncontext Sales {\n union Payer \"支払者\" { // discriminator `kind` by default\n CUSTOMER \"顧客\" {\n customerId: CustomerId\n }\n COMPANY \"法人\" {\n name: String\n billingEmail: EmailAddress?\n }\n }\n enum PaymentMethod { CARD TRANSFER }\n union PaymentDetails by method: PaymentMethod { // `by name`, optionally bound to an enum\n CARD { // exactly the enum's values (M015)\n last4: String\n }\n TRANSFER {\n bankCode: String\n }\n }\n}\n```\n\n- A union is a value type: usable like a value object (fields, `owns`, parameters, payloads, shared contexts).\n Variants hold value fields and invariants (seeing their own fields); entities and aggregates are not allowed.\n- In invariants, `payer.kind` compares with `Payer.COMPANY` (or the bound enum's values); variant fields are not\n accessible from outside the union.\n\n<!-- topic: shared -->\n### Shared contexts\n\n```dt\n// common/context.dt\nshared context Common \"共通\" { // shared kernel: usable by simple name from every context\n terms {\n currency: \"通貨\"\n }\n enum Currency { JPY USD }\n value Money {\n amount: Decimal\n currency: Currency\n }\n primitive EmailAddress: String { matches(value, \"^[^@\\s]+@[^@\\s]+$\") }\n}\n```\n\n- `Money` and `Common.Money` both work in other contexts. A context's own element with the same name wins (M014 warning).\n- A shared context holds only domain primitives, enums, value objects, unions, field sets and terms, and depends only on\n shared contexts. Its `terms` apply to fields of every context that has no term of its own for the name.\n\n<!-- topic: field-sets -->\n### Field sets\n\nFields that several aggregates, entities or events have for the same domain reason are written once as a field set\nand taken on with `with`.\n\n```dt\n// common/tenancy.dt\nshared context Common {\n /// 契約者(テナント)。業務データはテナントごとに分かれる。\n primitive TenantId \"テナントID\": UUID\n fields Tenanted \"テナント所属\" { // a field set: plain fields only\n tenant: TenantId \"テナント\"\n }\n}\n```\n\n```dt\n// expenses/expense.dt\ncontext Expenses \"経費\" {\n id ExpenseId \"経費申請ID\": UUID\n id StaffId \"職員ID\": UUID\n\n /// 規程により、申請の受付には実施者を記録する。\n fields Performed \"実施記録\" {\n performedBy: StaffId \"実施者\"\n }\n\n aggregate Expense \"経費申請\" with Tenanted { // tenant first, then its own fields\n id: ExpenseId\n applicant: StaffId \"申請者\"\n approver: StaffId? \"承認者\"\n invariant notSelfApproved \"申請者は承認できない\" { exists(approver) implies approver != applicant }\n\n create command submit(id: ExpenseId, applicant: StaffId \"申請者\", performedBy: StaffId \"実施者\") \"申請する\" // by proxy, too\n emits ExpenseSubmitted\n command approve(approver: StaffId \"承認者\", approvedAt: Timestamp \"承認日時\") \"承認する\"\n changes approver\n emits ExpenseApproved\n }\n\n event ExpenseSubmitted \"申請受付\" with Tenanted, Performed {\n expenseId: ExpenseId\n }\n event ExpenseApproved \"承認\" with Tenanted {\n expenseId: ExpenseId\n approver: StaffId \"承認者\" // a business role, named by the role\n approvedAt: Timestamp \"承認日時\" // when it happened in business terms\n }\n}\n```\n\n- `fields Name \"label\" { ... }` holds plain fields, written like any other field (type, multiplicity, label, `///`).\n No `owns`, `references`, invariants, `with` or `id` inside (M016).\n- `with A, B` after the name and label of an `aggregate`, `entity` or `event` (only those, M016). A field set is\n referenced like a type: by name in its own context and from shared contexts, as `Context.Name` otherwise.\n- The fields come first, in the order of `with`, and then count as the element's own: invariants see them, and an\n aggregate's states share them. A name may come from one place only, the element or one of its field sets (M016).\n- The identity of aggregates and entities is never a field set: every aggregate and entity declares `id` itself (M013).\n- A command's event should say which aggregate it is about: it needs a field of the aggregate's ID type, its own or\n from a field set (M017 warning). An event emitted by several aggregates needs the ID type of each (optional fields\n count).\n\n<!-- topic: fields -->\n### Domain fields and technical fields\n\nWrite a field in the DSL when **domain experts talk about it, or a business rule (an invariant, a transition, a\ncommand's decision) refers to it**. A field that is neither is technical: leave it out of the DSL and of the generated\ncode, and add it in your own code around the generated types (see [Technical metadata](#technical-metadata)).\n\n- **Required by the kind of element** (the ID of an aggregate or entity): enforced by the language (M013), not a field\n set.\n- **Chosen for some elements** (a tenant, a recorded performer): a field set and `with`.\n- **Technical**: event ID, the time the system recorded something (`recordedAt`), optimistic-lock `version`,\n `createdAt` / `updatedAt`, correlation / causation IDs, the system's acting account, schema versions.\n\nTwo cases look alike but are not:\n\n- **When**: the time something happened in business terms is domain — rules refer to it (\"within 30 days of\n confirmation\"). Name it by its meaning (`confirmedAt: Timestamp \"確定日時\"`). If every event is to carry its business\n time, say so with a field set: `fields Occurred { occurredAt: Timestamp \"発生日時\" }`. The time the system recorded it\n is technical. The two differ with late entry, offline work and imports.\n- **Who**: a business role (approver, person in charge) is domain — rules refer to it (\"the applicant cannot\n approve\"). Name it by the role (`approvedBy: StaffId \"承認者\"`), usually passed from the command's parameters to the\n event. If regulations require recording the performer of every operation,\n `fields Performed { performedBy: StaffId \"実施者\" }`. The login account, service account or IP address is\n technical; it differs with proxy entry and batches.\n\n<!-- topic: names -->\n### Names (labels and descriptions)\n\n```dt\n// billing/context.dt\n/// 注文への請求と支払いを扱う。\ncontext Billing \"請求\" {\n terms { // field / parameter names, defined once per context\n /// 請求書が支払われた日時。\n paidAt: \"支払日時\"\n }\n}\n```\n\n```dt\n// billing/invoice.dt\ncontext Billing { // other files open the context without a name\n id InvoiceId \"請求書ID\": UUID\n\n /// 注文1件に対する請求。\n aggregate Invoice \"請求書\" { // declarations: a string after the name\n id: InvoiceId\n references order: Sales.OrderId -> Sales.Order \"請求対象\" // an inline label on a field\n owns amount: Money \"請求金額\"\n state Open \"未払い\"\n state Paid \"支払済み\" {\n paidAt: Timestamp // → 支払日時 (from terms)\n }\n invariant positive \"金額が正\" { amount.amount > 0 }\n create command issue(id: InvoiceId, order: Sales.OrderId, amount: Money) \"請求書を発行する\" -> Open\n command pay(at: Timestamp \"支払日時\") \"支払う\"\n Open -> Paid\n emits InvoicePaid\n }\n enum InvoiceKind \"請求区分\" { Regular \"通常\" Refund \"返金\" }\n event InvoicePaid \"支払完了\" {\n invoiceId: InvoiceId\n paidAt: Timestamp\n }\n}\n```\n\n- Declarations (context, id, primitive, verified, enum and its values, value, union and its variants, entity,\n aggregate, state, command, invariant, event, error) take an optional string label after the name.\n- A field or parameter is named by, in order: its inline string → the context's `terms` → the name of its type\n (for `references`, the target aggregate) → the English name.\n- `///` lines directly above a declaration or term are its description.\n- Labels appear in the Live View and as JSDoc in the generated TypeScript; identifiers and generated type names stay\n English.\n\n<!-- topic: expressions -->\n### Invariant expressions\n\n- Literals: `true`, `false`, `42`, `-1`, `3.14`, `\"text\"`; enum values `CancelReason.Other`.\n- Field paths: `note`, `item.quantity`, `amount.amount`.\n- Operators, loosest first: `implies`, `or`, `and`, `== != < <= > >=`, `x in 1..9`, `not`.\n- Functions: `exists(x)` (an optional is present; narrows `x` on the right of `and` / `implies`), `count(c)`,\n `all(c, x => …)`, `any(c, x => …)`, `unique(c)`, `unique(c, x => key, …)`, `contains(c, x)`, and in domain\n primitives `length(value)`, `matches(value, \"regex\")`.\n- `unique(c)`: the elements of a collection are distinct. With keys, `unique(lines, l => l.sku)` means no two\n elements have the same key, and `unique(confirmations, c => c.scenario, c.reviewer)` that no two have the same\n combination of keys. A key cannot be optional (M009); to include an optional field, group the keys into a value\n object and use it as the key.\n- `contains(c, x)`: `x` is an element of `c`. A subset is written with `all`: `all(required, s => contains(confirmed, s))`.\n- Equality — `==`, `!=`, `unique` and `contains` compare alike: primitives and domain primitives by value, enum\n values by name, value objects by all their fields, entities by `id`, union values by discriminator and fields.\n Both sides must be of the same type (domain primitives also compare with literals, and over `Int` or `Decimal` with\n `count(…)` and `length(…)`, also in `<`, `<=`, `>`, `>=` and ranges); collections and optional values cannot be\n compared (guard an optional with `exists(...)` first).\n- An invariant must be a Bool expression. `dt check` checks only its type; whether a value holds it is checked by the\n generated check functions (`checkConfirmedOrder`, see [Generated checks](#generated-checks)), with exactly these\n semantics.\n- A constraint the DSL cannot express is declared without an expression, with a label or a `///` description that\n states it (M018): `invariant uniqueEmail \"メールアドレスは顧客ごとに一意\"`. It is documented like any invariant (in\n the Live View and as `@invariant uniqueEmail (…): not expressed in the DSL` in the generated TypeScript) and is\n checked by your code only: the generated checks take it as a function you pass in (a slot). In a domain primitive,\n its name joins the invariants of `Invalid<Name>`.\n- `count`, `exists`, `length`, `matches`, `all`, `any`, `unique` and `contains` are also usable as field and parameter\n names; followed by `(` they are the functions.\n\n<!-- topic: requires -->\n### Command requirements\n\n`requires` states when a command is accepted. Like an invariant it is a Bool expression, written after the\ntransitions, any number per command; `else` names the error the command returns when it does not hold:\n\n```dt\n// review/review.dt\ncontext Review {\n id ReviewId: UUID\n id PersonId: UUID\n value ApprovalPolicy {\n approvers: PersonId[1..*]\n }\n\n aggregate Review \"レビュー\" {\n id: ReviewId\n policy: ApprovalPolicy\n state Reviewing {\n openBlockers: Int\n }\n state Escalated {\n openBlockers: Int\n }\n state Accepted {\n acceptedBy: PersonId\n /// All the scenarios the policy requires are confirmed.\n invariant fullyConfirmed \"必要な確認がそろっている\" // holds whenever the review is Accepted\n }\n\n command accept(person: PersonId) \"受入\"\n Reviewing | Escalated -> Accepted\n requires authorizedApprover \"承認者である\" { // depends on the parameters and the current state\n contains(policy.approvers, person)\n } else NotApprover\n requires noOpenBlocker \"未解決の阻害がない\" { openBlockers == 0 } else Blocked\n /// The reviewer's own sign-off is recorded outside this model.\n requires signedOff \"本人の承認が記録済み\" else NotSignedOff // not expressed in the DSL\n errors Conflict\n }\n\n error NotApprover\n error Blocked\n error NotSignedOff\n error Conflict\n}\n```\n\n- **Invariant or requirement?** An invariant holds whenever the aggregate is in a state: it describes the state\n after a transition (an accepted review has all required confirmations). A requirement decides whether the command\n is accepted, from its parameters and the aggregate's current data (the person is among the approvers). If a\n condition reads \"after the command, …\", it is an invariant of the target state; if it reads \"the command may run\n only when …\", it is a requirement.\n- **Not for application-level authorization.** Whether the logged-in user has a role is checked by the application,\n not the model. Only when the policy is part of the aggregate's state (its approvers) is it a domain precondition.\n- **Scope**: the parameters; the aggregate's common fields (including those of field sets); and the fields that every\n source state of the command has, with the same name and type (with one source state, all its fields). A\n `create command` sees its parameters only, a command of an aggregate without states all its fields. Using another\n field is M010. Other aggregates cannot be referenced; pass what is needed as parameters.\n- A parameter may not be named like a field its command's requirements see (M019), so that a name in `requires` has\n one meaning. Commands without `requires` are not affected.\n- Name the fields of errors after their source, as for events ([naming payload fields](#command-events), M022): the\n parameters and the aggregate before the command.\n- `else Error` is required. The error joins the command's errors: it need not be listed in `errors` (listing it\n again is allowed), and several requirements may share one. An unknown error is M007.\n- Requirement names are unique within the command (M002). Without an expression, a requirement needs a label or a\n `///` description (M018), like an invariant.\n- They appear in the Live View (振る舞い tab) and as `@requires` JSDoc on the generated command type (see\n [Generated TypeScript](#generated-typescript)); your command implementation returns the error. The generated\n `check<Command>Requires` (e.g. `checkConfirmOrderRequires`) evaluates them, and the generated tests compare your\n implementation with it.\n- **When several requirements do not hold, the command returns the error of the first one in declaration order.**\n Order the requirements accordingly (the cheapest or most basic first); the generated tests expect exactly that\n error.\n- Not supported: requirements for some source states only (split the command instead), and a generic\n `<Command>Rejected` error.\n- `requires` and `else` are also usable as field and parameter names.\n\n<!-- topic: policies -->\n### Rules that depend on a policy\n\nA policy is a rule the business may change: how many approvals a purchase needs, who may approve, how a credit score\nis computed. There is no policy element; one question decides how to write a rule that depends on one:\n\n**Must the rule keep holding after the policy changes?** An invariant holds at all times, so an invariant over a\npolicy kept outside the aggregate would make existing aggregates invalid as soon as the policy changes. If the rule\nmust hold for this aggregate from now on, fix the policy on the aggregate and write an invariant (1). If the policy\ndecides only whether a command runs, under the policy current at that moment, write a requirement (2). The choice\nstates whether a policy change affects existing data.\n\n**1. Policy fixed on the aggregate**: keep the policy as a value object in the aggregate, taken when the aggregate is\ncreated, and write invariants over it. A later change of the policy applies to new aggregates only.\n\n```dt\n// purchasing/purchase-request.dt\ncontext Purchasing {\n id RequestId: UUID\n primitive Count: Int { value in 0..100 }\n value ApprovalPolicy {\n requiredApprovals: Count\n }\n\n aggregate PurchaseRequest \"購買申請\" {\n id: RequestId\n policy: ApprovalPolicy // the policy when the request was submitted\n approvedBy: PersonId[0..*]\n state Pending\n state Approved {\n invariant enoughApprovals { count(approvedBy) >= policy.requiredApprovals }\n }\n\n create command submit(id: RequestId, policy: ApprovalPolicy) -> Pending\n command approve(person: PersonId)\n Pending -> Pending | Approved\n changes approvedBy\n }\n}\n```\n\n`requiredApprovals` may be an `Int` or, as here, a domain primitive over `Int` or `Decimal`: a domain primitive\ncompares with the numbers `count(…)` and `length(…)` compute.\n\n**2. The current policy decides only whether a command runs**: pass the policy as a parameter of the command and\nstate the condition with `requires`. Existing aggregates stay valid whatever the policy becomes.\n\n```dt\n// purchasing/supplier.dt\ncontext Purchasing {\n id SupplierId: UUID\n id PersonId: UUID\n value ApproverList {\n approvers: PersonId[1..*]\n }\n\n aggregate Supplier \"取引先\" {\n id: SupplierId\n state Candidate\n state Listed\n\n command list(person: PersonId, current: ApproverList)\n Candidate -> Listed\n requires authorized { contains(current.approvers, person) } else NotApprover\n }\n\n error NotApprover\n}\n```\n\n**3. The policy picks a kind of rule**: an enum (or a union, when the kinds carry data) in the aggregate, and one\ninvariant or requirement per kind with `implies`.\n\n```dt\n// purchasing/contract.dt\ncontext Purchasing {\n id ContractId: UUID\n enum SignOff { Single Dual }\n\n aggregate Contract \"契約\" {\n id: ContractId\n signOff: SignOff\n signedBy: PersonId[0..*]\n invariant dualSignOff { signOff == SignOff.Dual implies count(signedBy) >= 2 }\n }\n}\n```\n\n**4. The policy is code** (scores, rule engines, pluggable strategies): declare the invariant or requirement without\nan expression, with a label or `///` description. Your code passes its check to the generated `…With(checks)`\n(`dt guide checks`), which can apply the policy.\n\n```dt\n// purchasing/credit-line.dt\ncontext Purchasing {\n id CreditLineId: UUID\n\n aggregate CreditLine \"与信枠\" {\n id: CreditLineId\n state Open\n\n command extend(amount: Decimal)\n Open -> Open\n requires creditworthy \"与信スコアが基準以上\" else NotCreditworthy // scored by your code\n }\n\n error NotCreditworthy\n}\n```\n\n- Not supported: arithmetic in expressions (`limit * 2`, sums over a collection). Use (4) when a rule needs it.\n- Other aggregates cannot be read from an expression: the policy is a value passed in or kept, never looked up.\n- **A rule across aggregates** is written as one of: the values it needs passed as parameters and checked with\n `requires` (2); the evidence of a check made outside the aggregate, a verified type (`dt guide verified`); a reaction\n to another aggregate's events, a policy (`dt guide reactions`); or a boundary drawn so that the data is in one\n aggregate.\n\n<!-- topic: changes -->\n### Command changes\n\n`changes` lists the fields a command may change, after `requires` and before `emits`. What a command does to the\nother fields follows from its transition:\n\n```dt\n// tickets/ticket.dt\ncontext Tickets {\n id TicketId: UUID\n id PersonId: UUID\n\n aggregate Ticket {\n id: TicketId\n title: String\n note: String?\n state Open {\n assignee: PersonId?\n }\n state InProgress {\n assignee: PersonId\n startedAt: Timestamp\n }\n state Closed {\n closedAt: Timestamp\n }\n\n create command report(id: TicketId, title: String) -> Open // a factory sets every field\n command retitle(title: String, note: String?)\n Open -> Open\n InProgress -> InProgress\n changes title, note // common fields\n command assign(assignee: PersonId?)\n Open -> Open\n changes assignee // carried over from Open to Open, so listed to be changed\n command start(assignee: PersonId, at: Timestamp) Open -> InProgress // no `changes`: sets InProgress's assignee and startedAt, keeps the rest\n command close(at: Timestamp) Open | InProgress -> Closed // sets closedAt; assignee and startedAt are dropped\n }\n}\n```\n\n1. Without `changes`, a command changes **no** common field (strict, like `emits`).\n2. A state-specific field that both the source and the target state have, with the same name and type, is **carried\n over**: it keeps its value, unless it is listed in `changes`. `assignee` is carried over by `assign`\n (`Open -> Open`), not by `start`: it is `PersonId?` in Open and `PersonId` in InProgress.\n3. The target state's other own fields are new, and the command always sets them, without `changes` (`startedAt`,\n and `assignee` in `start`). The source state's own fields that the target does not carry over are dropped.\n4. A `create command` sets every field; `changes` on it is an error.\n5. Without states, a command changes only the fields in `changes`.\n6. A field in `changes` is **given every time**: there is no \"absent = keep\". An optional field is cleared by giving\n `undefined`. A command that changes a field only in some cases gives the old value in the others.\n7. The unit is a top-level field: `changes items` for adding, removing or editing elements of `items`.\n\nThe generated decision of each command enforces these rules (see [Decisions and `decide`](#decisions-and-decide)).\n\n- M021: the names are fields of the aggregate — common fields (also from field sets) or fields of its states — other\n than its identity; not on a `create command`. A state-specific field must be carried over by at least one outcome of\n the command (from a state that has it to one that has it, with the same type): in the other outcomes it is new in\n the target and set anyway, or not in the target and dropped, so listing a field that no outcome carries over would\n change nothing. `retitle` above may list `assignee` (`Open -> Open` carries it over); `start` may not.\n- A field is listed once (M002).\n- `changes` is also usable as a field and parameter name.\n\n<!-- topic: events -->\n### Command events\n\n`emits` is the exact list of the events a command emits **when it succeeds**; on failure (an error) it emits none.\nEach entry is read like a field:\n\n```dt\n// work/work.dt\ncontext Work {\n id WorkId: UUID\n id StepId: UUID\n\n aggregate Work {\n id: WorkId\n state Draft\n state InProgress\n state Completed {\n completedAt: Timestamp\n }\n\n create command open(id: WorkId)\n -> Draft\n emits WorkOpened // the new ID is a parameter\n command plan(steps: StepId[1..*])\n Draft -> Draft\n emits StepPlanned[1..*] // one per step\n command rename(title: String)\n Draft -> Draft\n emits Renamed? // not when the title is the same\n command start() Draft -> InProgress\n command progress(step: StepId, at: Timestamp)\n InProgress -> InProgress | Completed\n emits StepRecorded,\n WorkCompleted when -> Completed // only when it ends in Completed\n }\n\n event WorkOpened {\n workId: WorkId\n }\n event StepPlanned {\n workId: WorkId\n step: StepId\n }\n event Renamed {\n workId: WorkId\n title: String\n }\n event StepRecorded {\n workId: WorkId\n step: StepId\n }\n event WorkCompleted {\n workId: WorkId\n completedAt: Timestamp\n }\n}\n```\n\n- `emits X`: **exactly one** `X`. `X?`: at most one. `X[0..*]`: any number. `X[1..*]`: one or more. `X[n..m]`:\n between `n` and `m` (the generated types check the cases before; other bounds are documented only).\n- `X when Sources -> Targets`: the entry applies only to the outcomes that match — the command starts in one of the\n sources and ends in one of the targets. Either side may be left out (`when Confirmed ->`, `when -> Completed`), not\n both; each side may list states with `|`. A multiplicity comes before `when`: `ItemAdded[1..*] when -> Draft`.\n- The states of `when` must be sources (left) or targets (right) of the command's transitions, and the pattern must\n match at least one of its outcomes (M006). A `create command` has no source: only `when -> X`. Commands of an\n aggregate without states take no `when`.\n- An event is listed once per command (M011): its events are keyed by name in the generated result, so two events of\n different contexts with the same name cannot be emitted by one command.\n- The events of one command have **no order**: they are facts of one decision. With event sourcing, applying the\n events of one command must not depend on their order.\n- **When the contract and the implementation disagree**, follow `@see` on the generated type to the declaration,\n and change the DSL, not the implementation. If following the contract would make the implementation do something\n wrong in business terms — emitting `Renamed` although the title did not change — the contract is what is wrong:\n write `emits Renamed?`. Never emit an event only to satisfy the types, and never leave out one the contract requires.\n- `when` is also usable as a field and parameter name.\n\n**Naming payload fields.** Name the fields of events (and errors) after their source, so that the implementation does\nnot have to guess where a value comes from: a parameter (`step` above), a field of a parameter (`sku` from\n`item: OrderItem`), or a field of the aggregate after the command (`completedAt` of `Completed`). A value computed in\nthe implementation (a total, the days overdue) needs no source. A field that has no source of the same name and type,\nwhile some source has its type, is a warning (M022): `progress(step: StepId, at: Timestamp)` emitting\n`StepRecorded { recordedAt: Timestamp }` would be one — name the parameter `recordedAt`. Exempt are fields of the\naggregate's ID type (M017) and of type Int, Decimal or Bool (counts, sums and flags are mostly computed). Types match\nby base type, optionality aside; a collection only a collection of the same element type. An error's sources are the\nparameters and the aggregate **before** the command (its source states; none for a `create command`). A declarative\nmapping (`emits X { a = b }`) is deferred (see ADR 0021).\n\n<!-- topic: pure -->\n### Pure commands\n\nA command is a **pure function** of the aggregate and its parameters: no I/O, no clock, no randomness, no generated\nIDs. Whatever the decision needs comes in as a parameter — the new aggregate's ID (`open(id: WorkId)`), the time\n(`at: Timestamp`), data of other aggregates. Loading, saving, publishing and calling other services happen around the\ncommand, in your application code. This makes every command reproducible: the same inputs give the same aggregate\nand events, so commands are tested without mocks.\n\nA `create command` without a parameter of the aggregate's ID type is a warning (M020): it would have to make the ID\nitself.\n\nA check that needs more than the aggregate and the parameters — no other customer uses the email address — is made\nbefore the command, and the command takes its evidence as a parameter: a verified type (see\n[Verified types](#verified-types)).\n\n<!-- topic: verified -->\n### Verified types\n\nCommands are pure, so they cannot check what lies outside their aggregate: an email address no other customer uses, an\naddress an outside service confirmed, a budget approved, stock reserved. A verified type is the **evidence that such a\ncheck was made**: a value of a base type that only its factory makes. A command that takes it as a parameter cannot be\ncalled without the check; skipping it is a compile error.\n\n```dt\n// purchasing/purchase-order.dt\ncontext Purchasing {\n id PurchaseOrderId: UUID\n id BudgetId: UUID\n value Spend {\n budget: BudgetId\n amount: Money\n }\n /// 予算の残高で賄えると、発注のときに確かめた支出。\n verified CoveredSpend \"予算内の支出\" of Spend errors OverBudget\n /// 予算内と確かめたうえで、承認者の決裁上限以内と確かめた支出(両方の確認を持つ)。\n verified ApprovedSpend \"承認済みの支出\" of CoveredSpend for PersonId errors OverApprovalLimit, NotApprover\n\n aggregate PurchaseOrder \"発注\" {\n id: PurchaseOrderId\n spend: Spend // stored as the base type\n state Draft\n state Placed\n\n create command open(id: PurchaseOrderId, spend: Spend) -> Draft\n command place(spend: ApprovedSpend) // both checks were made\n Draft -> Placed\n changes spend\n }\n\n error OverBudget\n error OverApprovalLimit\n}\n```\n\n- `verified Name \"label\" of Base for Context? errors E1, E2`. The base is a value type: a primitive type, a domain\n primitive, an ID, an enum, a value object, a union, or another verified type — then the value carries both checks.\n Not an entity or an aggregate (M024).\n- `for` names what the check needs besides the value: the customer to leave out (`for CustomerId?`: absent when a\n customer registers), a tenant, the approver. `?` makes it optional. It is a value type, not a verified one (M024).\n Without `for`, the check needs the value only.\n- `errors` are the errors of the factory, not of the commands that take the value: `changeEmail` above returns no\n `EmailTaken`; the factory did, before the command.\n- A verified type types **command parameters** (and the base of another verified type) only. It is the evidence of a\n check at one moment, so a field of an aggregate, entity, value object, union, field set, event or error stores the\n base type instead (M024, with the hint `store the base type 'Spend'`). A command stores a verified parameter in a\n field of the base type as it is.\n- In invariants and requirements a verified value is a value of its base type (`requires` can compare\n `spend.amount`), and an event or error field of the base type takes its value from a verified parameter of the same\n name (M022).\n- **Use it only for facts that need the outside world**: other aggregates, outside services, the state of the world\n at that moment. A rule over the command's parameters and the aggregate is an invariant or a `requires` (see\n [Command requirements](#command-requirements)); a rule over a policy, see [Rules that depend on a\n policy](#rules-that-depend-on-a-policy).\n- **Evidence of the moment, not a guarantee when the aggregate is saved.** Two customers registering the same address\n at once may both pass the check. Keep enforcing the rule where the data is stored (a unique index), and document it\n on the aggregate as an invariant without an expression (`uniqueEmail` above, in `dt guide aggregates`).\n- `verified`, `of` and `for` are also usable as field and parameter names.\n\nThe generated code (`examples/shop`):\n\n```ts\n/**\n * 未使用のメールアドレス\n * 他の顧客に使われていないことを、登録・変更のときに確かめたメールアドレス。\n * A value of Common.EmailAddress for which MakeAvailableEmail made a check outside the aggregate. Evidence of a check made at that moment, not a guarantee when the aggregate is saved — keep enforcing it where it is stored (e.g. a unique index).\n * Make it with your `MakeAvailableEmail` factory; never `as AvailableEmail` outside it (see dt guide lint).\n * @see sales/customer.dt:5\n */\nexport type AvailableEmail = Common.EmailAddress & { readonly 'verified:Sales.AvailableEmail': true };\n\nexport type MakeAvailableEmail = (value: Common.EmailAddress, context: CustomerId | undefined) => Promise<Result<AvailableEmail, EmailTaken>>;\n```\n\n- The type is the base with a property of its own, one per verified type: a value of the base is not one\n (`Argument of type 'EmailAddress' is not assignable to parameter of type 'AvailableEmail'`), a value with another\n check is not one either, and a value with two checks has both properties. It is assignable to its base, so it is\n stored in a field of the base type without a conversion.\n- The factory `Make<Name>` is async (the check reads repositories and services) and returns a `Result`: the value, or\n one of `errors` (`never` without `errors`, still a `Result`, so every factory is called the same way). Its second\n parameter is the context of `for`: required with `for X`, `X | undefined` with `for X?`, absent without `for`.\n- **You write the factory**, with your repositories and services. It is the only code that may assert the type\n (`email as Sales.AvailableEmail`); a cast anywhere else defeats the purpose — the same weakness as the constructors\n of domain primitives.\n\n```ts\n// examples/shop/src/app/verifications.ts\nexport function verifications(customers: CustomerRepository): Verifications {\n return {\n // `customer` is the one whose email changes, so that keeping one's own address is not \"taken\"; none on register.\n async makeAvailableEmail(email, customer) {\n const holder = await customers.find((c) => c.email === email && c.id !== customer);\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the factory of the verified type: the check above is the evidence\n return holder ? err({ type: 'EmailTaken', email }) : ok(email as Sales.AvailableEmail);\n },\n };\n}\n```\n\nThe generated contract tests do not test the factories (see [Generated tests](#generated-tests)).\n\n<!-- topic: reactions -->\n### Reactions to events (policies)\n\nA policy states a business rule of the form **\"when event X happens, command Y is eventually run\"** — Event\nStorming's policy (the lilac sticky between an event and a command). It is declared in the context that reacts:\n\n```dt\n// billing/policies.dt\ncontext Billing {\n /// 確定した注文は、その合計で請求する。\n policy BillConfirmedOrder \"確定した注文に請求する\" when Sales.OrderConfirmed -> Invoice.issue\n}\n```\n\n- `policy Name \"label\" when Event -> Aggregate.command`. The aggregate is one of the policy's own context, named\n without its context; the event may be of any context (`Sales.OrderConfirmed`). A shared context has no policies.\n- **Only the reaction.** How the command's arguments come from the event, conditions, compensation and ordering are not\n part of the DSL; your code computes the arguments (below).\n- **\"Eventually\", by any mechanism.** The rule holds whether the command runs in the same use case, through an event\n bus, from a queue or by a nightly batch. Nothing in the generated code chooses or forces a mechanism.\n- Declaring a reaction is optional. Declare the ones across contexts at least: they are the dependencies of the\n context map (a policy counts as an **event** dependency of the reacting context on the event's context), and\n `dt show`, the Live View and the language server show them on the command (\"triggered by\") and on the event.\n- M023: the event exists, the aggregate is one of the policy's context (a policy naming an aggregate of another\n context is declared in the wrong context: the hint names the one), the command is one of the aggregate; two\n policies of a context that state the same reaction are a warning. Names are unique in the context (M002).\n- `policy` is also usable as a field and parameter name.\n\nFor each policy, `dt generate ts` writes the type of its **handler**, from the event to what the application needs to\nrun the command, the command's **input**: its parameters by name, after the ID of the aggregate to run it on (named\nlike the aggregate) unless it is a `create command`. An input is written once per command, also when several policies\nrun it.\n\n```ts\n/**\n * What the application needs to run Invoice.issue (a factory): its parameters, by name.\n * Given by the handlers of the policies that run it.\n * @see billing/invoice.dt:14\n */\nexport interface IssueInvoiceInput {\n /** 請求書ID */\n readonly id: InvoiceId;\n /** 注文ID */\n readonly order: Sales.OrderId;\n /** 請求金額 */\n readonly billedAmount: Common.Money;\n}\n\n/**\n * Policy BillConfirmedOrder: when Sales.OrderConfirmed happens, Invoice.issue is run — eventually.\n * …\n */\nexport type BillConfirmedOrderPolicy = (event: Sales.OrderConfirmed) => Promise<IssueInvoiceInput>;\n```\n\nThe handler computes the input: data from elsewhere (the order to bill) and new IDs come in through closures your code\nbuilds, and a handler that cannot compute the input rejects. Your application then runs the command with it, by\nwhatever mechanism.\n\n**Whether handlers are required** is a choice of the configuration (see [Configuration](#configuration)):\n`generate.ts.policies`, overridden per context by `generate.ts.contexts.<Context>.policies`:\n\n- `optional` (default): only the types above; nobody has to use them.\n- `required`: also `<Context>PolicyHandlers`, one handler per policy of the context, and the generated contract tests\n of that context require them (`contract({ …, policies })`; a missing handler is a compile error). They feed each\n handler generated events and check that it resolves to an input the command accepts (see\n [Generated tests](#generated-tests)).\n\n```ts\n// examples/shop/src/app/policies.ts\nexport function billingPolicies({ loadOrder, newInvoiceId }: PolicyDeps): Billing.BillingPolicyHandlers {\n return {\n // a confirmed order is billed for its total\n BillConfirmedOrder: async (event) => {\n const order = await loadOrder(event.orderId);\n if (order?.state !== 'Confirmed') throw new Error(`order ${event.orderId} is ${order ? order.state : 'not found'}, not Confirmed`);\n return { id: newInvoiceId(), order: order.id, billedAmount: orderTotal(order) };\n },\n };\n}\n```\n\n<!-- topic: generated -->\n## Generated TypeScript\n\nPer context a module (`Sales.ts`, …) with ID brands, enums, value objects, entities, unions, a discriminated union per\nstateful aggregate (`DraftOrder | ConfirmedOrder | …`), event / error types with a `type` tag, a function type per\ncommand and an `<Aggregate>Commands` interface to implement, a decision type per command and a shape constant per\naggregate for `decide` (see [Decisions and `decide`](#decisions-and-decide)), plus `index.ts` and `_runtime.ts`. A\ndomain primitive gets a branded type, a constructor contract `Make<Name>` and a generated constructor `make<Name>`;\na verified type its base with a property of its own and the contract of its factory `Make<Name>`, which you write (see\n[Verified types](#verified-types)); invariants and requirements get check functions (see\n[Generated checks](#generated-checks)); a policy the type of its handler and the input of its command (see\n[Reactions to events](#reactions-to-events-policies)). A field set is an interface that the elements using it extend (`interface Expense extends Common.Tenanted`; for a stateful aggregate, its\n`…Common`). The JSDoc of every declaration that comes from the DSL ends with `@see <file>:<line>`, the declaration's\nplace as `dt check` prints it (relative to the config root, e.g. `@see sales/order.dt:51`); editing the `.dt` sources\nmoves these lines, and generating again rewrites them.\n\nThe examples in this section are generated from `examples/shop` in the repository, a fuller version of the Sales\ncontext of the DSL examples (with labels, and `emits NoteChanged?` on `changeNote`).\n\nUnions to name groups of them, each generated only when it has a member:\n\n| Type | Members |\n|---|---|\n| `DomainEvent` | every event of the context |\n| `PersistentEvent` | the events declared `persistent` |\n| `Aggregate` | every aggregate of the context |\n| `<Aggregate>Event` (`OrderEvent`) | the events the aggregate's commands may emit (also of other contexts) |\n\n```ts\n/**\n * Order.confirm (Draft -> Confirmed)\n * 注文を確定する\n * @param at 日時\n * @requires hasItems (明細が1件以上) else EmptyOrder: count(items) > 0\n * @changes none — the command changes no common field\n * @emits OrderConfirmed — exactly once when the command succeeds\n * @see sales/order.dt:51\n */\nexport type ConfirmOrder = (\n order: DraftOrder,\n at: Timestamp,\n) => Result<{\n readonly Confirmed: {\n readonly aggregate: ConfirmedOrder;\n readonly events: { readonly OrderConfirmed: OrderConfirmed };\n };\n}, EmptyOrder>;\n```\n\nThe success value is **keyed by the outcome**: the target state's name (an aggregate without states: the aggregate's\nname, `{ Customer: { aggregate, events } }`), holding the aggregate in that state and its `events`. Every command has\nthis shape, also with one target. With several targets the value is a union, and each branch rules out the other keys,\nso a value holds exactly one outcome:\n\n```ts\nexport type ProgressWork = (\n work: InProgressWork,\n step: StepId,\n at: Timestamp,\n) => Result<\n | {\n readonly InProgress: {\n readonly aggregate: InProgressWork;\n readonly events: { readonly StepRecorded: StepRecorded };\n };\n readonly Completed?: never;\n }\n | {\n readonly Completed: {\n readonly aggregate: CompletedWork;\n readonly events: { readonly StepRecorded: StepRecorded; readonly WorkCompleted: WorkCompleted };\n };\n readonly InProgress?: never;\n },\n never\n>;\n```\n\n`events` is always there, one property per event of that outcome, keyed by the event's name (its `type`; an event of\nanother context by its simple name) and typed by its multiplicity:\n\n| `emits` | property of `events` |\n|---|---|\n| `X` | `X: X` |\n| `X?` | `X?: X` |\n| `X[0..*]` | `X: readonly X[]` (also when empty) |\n| `X[1..*]` | `X: readonly [X, ...X[]]` |\n| `X[n..m]` (others) | `X: readonly X[]`, the bounds in JSDoc (not checked by the types) |\n| none | `NoEvents` (only `{}` is assignable) |\n\nThe JSDoc says the same in words, one `@emits` line per entry (`@emits WorkCompleted — exactly once when it ends in\nCompleted`). Return the value with `ok`, which takes its type from the contract, so that the compiler checks the\nobject exactly: a missing event is reported as `Property 'WorkCompleted' is missing …`, an event the outcome does not\nemit as `… 'WorkCompleted' does not exist in type …`:\n\n```ts\nreturn ok({ Confirmed: { aggregate: confirmed, events: { OrderConfirmed: { type: 'OrderConfirmed', orderId: order.id, confirmedAt: at } } } });\n```\n\nA command takes the aggregate in any source state of its transitions. When every source state has the same outputs\n(`Draft | Confirmed -> Cancelled`), it is a plain function, as above. When the outputs differ by source state — the\ntargets differ by transition, or the events do (`emits RefundRequested when Confirmed ->`) — the command is generic\nover the input state, and its result is the one of that state, looked up in an object keyed by the source states. A\ncaller with a `DraftOrder` gets a `DraftOrder` back, never a `ConfirmedOrder`; a caller with\n`DraftOrder | ConfirmedOrder` gets either:\n\n```ts\nexport type ChangeNoteOrder = <S extends DraftOrder | ConfirmedOrder>(\n order: S,\n note: string | undefined,\n) => {\n /** @emits NoteChanged? — at most once when the command succeeds */\n readonly Draft: Result<{\n readonly Draft: {\n readonly aggregate: DraftOrder;\n readonly events: { readonly NoteChanged?: NoteChanged };\n };\n }, never>;\n /** @emits NoteChanged? — at most once when the command succeeds */\n readonly Confirmed: Result<{\n readonly Confirmed: {\n readonly aggregate: ConfirmedOrder;\n readonly events: { readonly NoteChanged?: NoteChanged };\n };\n }, never>;\n}[S['state']];\n```\n\n<!-- topic: decide -->\n### Decisions and `decide`\n\nA command is implemented as a **decision**: for its outcome, the fields to `set` and the `events`. `decide` from the\nruntime turns a decision into the command, making the aggregate. Every command has a `<Command><Aggregate>Decision`\ntype with **one handler per source state**, keyed by the state's name — also with a single source state, so that every\ncommand is written the same way. A `create command` and a command of an aggregate without states have one function\ninstead.\n\n```ts\n/**\n * Order.confirm, decided per source state; implement it with `decide(OrderShape, …)` from the runtime.\n * Each outcome gives the fields of `set`, and `decide` makes the aggregate, taking the other fields from the input.\n * @requires hasItems (明細が1件以上) else EmptyOrder: count(items) > 0\n * @changes none — the command changes no common field\n * Draft -> Confirmed — set: confirmedAt (new in Confirmed); carried over: id, items, customer, note\n * @see sales/order.dt:51\n */\nexport type ConfirmOrderDecision = {\n /** @emits OrderConfirmed — exactly once when the command succeeds */\n readonly Draft: (order: DraftOrder, at: Timestamp) => Result<{\n readonly Confirmed: {\n readonly set: { readonly confirmedAt: Timestamp };\n readonly events: { readonly OrderConfirmed: OrderConfirmed };\n };\n }, EmptyOrder>;\n};\n```\n\n`set` holds exactly what the command decides, by the rules of [Command changes](#command-changes): the target state's\nnew fields and the fields in `changes`. Every one is required; an optional field is typed `T | undefined`, and an\noutcome with nothing to set has `NoChanges` (only `{}` is assignable). Anything else is a compile error that names the\nfield: `items` in the `set` of `confirm` is reported as\n`'items' does not exist in type '{ readonly confirmedAt: Timestamp; }'`, a missing `confirmedAt` as\n`Property 'confirmedAt' is missing in type '{}' …`. The JSDoc lists `@changes` and, per outcome, what is set, carried\nover and dropped.\n\n`decide(shape, decision)` calls the handler of the aggregate's `state` with the command's arguments. For the outcome\nit returns, it makes the aggregate from the fields of `set`, the other fields of the target state taken from the input\n(the common fields and the carried-over state-specific fields) and the target `state`; the input's other fields are\ndropped. A factory's aggregate is its `state` and `set`. Events and errors pass through unchanged. The shape is the\ndata `decide` needs, a constant generated per aggregate:\n\n```ts\n/**\n * The fields of Order as data for `decide`: the common fields, and the fields of each state on top of them.\n * @see sales/order.dt:13\n */\nexport const OrderShape: Shape = {\n fields: ['id', 'items', 'customer', 'note'],\n states: { Draft: [], Confirmed: ['confirmedAt'], Cancelled: ['reason'] },\n};\n```\n\nPass the decision type as the type argument, so that the handlers are typed from it. The result is assignable to the\ncommand's contract without a type assertion, and each handler is checked against its own transition — deciding the\nConfirmed outcome in the `Draft` handler of `changeNote`, or leaving a handler out, is a compile error:\n\n```ts\n// src/note.ts — your code\nimport { Sales, decide, err, ok } from '../generated/domain/index.js';\n\nconst noteChanged = (order: Sales.Order, note: string | undefined) =>\n note === order.note ? {} : { NoteChanged: { type: 'NoteChanged', orderId: order.id, note } as const };\n\nexport const changeNote: Sales.OrderCommands['changeNote'] = decide<Sales.ChangeNoteOrderDecision>(Sales.OrderShape, {\n Draft: (order, note) => ok({ Draft: { set: { note }, events: noteChanged(order, note) } }),\n Confirmed: (order, note) => ok({ Confirmed: { set: { note }, events: noteChanged(order, note) } }),\n});\n\nexport const confirm: Sales.OrderCommands['confirm'] = decide<Sales.ConfirmOrderDecision>(Sales.OrderShape, {\n Draft: (order, at) => {\n if (order.items.length === 0) return err({ type: 'EmptyOrder' });\n return ok({ Confirmed: { set: { confirmedAt: at }, events: { OrderConfirmed: { type: 'OrderConfirmed', orderId: order.id, confirmedAt: at } } } });\n },\n});\n```\n\nThe contract can still be implemented directly, returning the aggregate itself; then nothing keeps the command from\nchanging a field it does not list, and a generic contract needs a type assertion on its result (TypeScript does not\nnarrow `S` inside the implementation).\n\nCode that handles any command — saving the aggregate, publishing the events — uses `outcome` and `eventList` from the\nruntime. `outcome(value)` gives `{ to, aggregate, events }` (`to` is the key: the target state). `eventList(events)`\ngives the events as one list: arrays flattened, absent optional events skipped, sorted by event type (an order of\nconvenience: the contract has none).\n\n```ts\n// src/confirm.ts — your code: the application around a command\nimport { eventList, outcome, type Sales } from '../generated/domain/index.js';\n\nexport interface OrderStore {\n save(order: Sales.Order, events: readonly Sales.OrderEvent[]): Promise<void>;\n}\n\nexport async function confirm(commands: Sales.OrderCommands, store: OrderStore, order: Sales.DraftOrder, at: Date): Promise<Sales.EmptyOrder | undefined> {\n const r = commands.confirm(order, at); // the time is a parameter: the command is pure\n if (!r.ok) return r.error;\n const { aggregate, events } = outcome(r.value);\n await store.save(aggregate, eventList(events));\n return undefined;\n}\n```\n\nA command's requirements are documented on its type and on its decision, one `@requires` line each, and the\nerrors of their `else` are part of its error type (after those of `errors`, each once):\n\n```ts\n/**\n * Review.accept (Reviewing | Escalated -> Accepted)\n * 受入\n * @requires authorizedApprover (承認者である) else NotApprover: contains(policy.approvers, person)\n * @requires noOpenBlocker (未解決の阻害がない) else Blocked: openBlockers == 0\n * @requires signedOff (本人の承認が記録済み) else NotSignedOff: not expressed in the DSL\n * @changes none — the command changes no common field\n * @emits nothing\n * @see review/review.dt:24\n */\nexport type AcceptReview = (\n review: ReviewingReview | EscalatedReview,\n person: PersonId,\n) => Result<{\n readonly Accepted: {\n readonly aggregate: AcceptedReview;\n readonly events: NoEvents;\n };\n}, Conflict | NotApprover | Blocked | NotSignedOff>;\n```\n\n<!-- topic: your-code -->\n### Generated code and your code\n\nThe generated files are the **contract**, your code is the **implementation**:\n\n- Generated: types — the shapes of the model, the command signatures (`ConfirmOrder`, `OrderCommands`) and their\n decisions (`ConfirmOrderDecision`), and the constructor signatures of domain primitives (`MakeQuantity`) — plus\n data without logic: one shape constant per aggregate (`OrderShape`), the field names `decide` needs — plus logic\n derived 1:1 from DSL expressions: the checks of invariants and requirements, the equality of value types and the\n constructors of domain primitives (see [Generated checks](#generated-checks)). Invariants and requirements also\n appear as JSDoc (`@invariant`, `@requires`). `_runtime.ts` holds a few helpers (`ok`, `err`, `decide`, `outcome`,\n `eventList`, `parseDecimal`, and those of the checks) that are the same for every model.\n- Yours: the behaviour — command implementations (returning the error of the first requirement that does not hold;\n pure functions, see [Pure commands](#pure-commands)), the factories of verified types (the checks outside the\n aggregate, see [Verified types](#verified-types)) and the constraints the DSL does not express, which you pass to\n the generated checks — in your own files, which import the generated modules (never the other way round) and are\n type-checked against them. Your code may call the generated checks, or implement its own (a hand-written\n `Make<Name>` like the one below); the generated tests use the generated checks as the oracle either way.\n\n```ts\n// src/primitives.ts — your code\nimport { err, ok, type Sales } from '../generated/domain/index.js';\n\nexport const makeQuantity: Sales.MakeQuantity = (value) =>\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the factory: the check is on this line\n Number.isInteger(value) && value >= 1 && value <= 999 ? ok(value as Sales.Quantity) : err({ type: 'InvalidQuantity', value, invariant: 'valid' });\n```\n\nDo not edit generated files: change the `.dt` sources and generate again. Adding properties or methods to generated\ntypes (declaration merging, editing the output) is not supported; write functions that take the generated types.\n\nA complete example is [`examples/shop`](https://github.com/hisamekms/domaintrail/tree/main/examples/shop) in the\nrepository: the model, what `dt generate ts` writes from it, and the code written against it — the command\nimplementations and a small application layer around them (repositories, an event store, policies across contexts),\nwith the contract tests and a demo.\n\n<!-- topic: lint -->\n### Linting your code\n\nA domain type is a brand: `Sales.OrderId` is a `string` the compiler tells apart, `Common.Amount` a `Decimal` that holds\nits invariants, `Sales.AvailableEmail` the evidence of a check. A cast (`'x' as Sales.OrderId`, `amount as\nCommon.Amount`) makes one without its constructor, so nothing checks the invariants or makes the check: never cast to\na domain type, a verified type or `Decimal`. Make them instead:\n\n| Type | Make it with |\n|---|---|\n| ID, domain primitive | `make<Name>` (generated; or your own `Make<Name>`), e.g. `Sales.makeOrderId('order-1')` |\n| verified type | its factory `Make<Name>`, which you write (see [Verified types](#verified-types)) |\n| `Decimal` | `parseDecimal(text)` from the runtime: digits with an optional `-` and fraction, else `undefined` |\n\nA value that cannot fail to be valid (a sum of non-negative amounts is an `Amount`) still goes through its constructor;\na failure there is a bug, so throw. In tests, make the values the same way and assert the result rather than cast a\nfallback. The generated JSDoc of every branded type says how to make it (`Make it with makeAmount; never as Amount`).\n\nLet the linter enforce it with the type-aware rule that reports narrowing casts (it allows `as const` and widening):\noxlint's `typescript/no-unsafe-type-assertion` (with `--type-aware`, which needs `oxlint-tsgolint`) or typescript-eslint's\n`@typescript-eslint/no-unsafe-type-assertion`. Leave the generated directories out: they make the domain types, so they\ncast.\n\n```jsonc\n// .oxlintrc.json — run `oxlint --type-aware`\n{\n \"plugins\": [\"typescript\"],\n \"rules\": { \"typescript/no-unsafe-type-assertion\": \"error\" },\n \"ignorePatterns\": [\"generated/**\"]\n}\n```\n\n```js\n// eslint.config.js — typescript-eslint with type information\nimport tseslint from 'typescript-eslint';\n\nexport default tseslint.config(\n { ignores: ['generated/**'] },\n {\n files: ['**/*.ts'],\n extends: [tseslint.configs.base],\n languageOptions: { parserOptions: { projectService: true } },\n rules: { '@typescript-eslint/no-unsafe-type-assertion': 'error' },\n },\n);\n```\n\nThe legitimate casts are in factories only — your `Make<Name>`, a verified type's factory — each with a disable\ncomment that says why: `// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the factory: …` (ESLint:\n`// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- …`).\n\n`dt generate ts` prints `hint: no lint rule against casts to domain types found (no-unsafe-type-assertion); see dt guide\nlint` (in `--format json`, in `hints`) when no lint config mentions the rule: it looks for `.oxlintrc.json`,\n`.oxlintrc.jsonc`, `oxlint.config.*` and `eslint.config.*` in the config's directory and the directories above it up to\nthe top of the git repository (only the config's directory without one). It is a heuristic — the files are searched as\ntext for `no-unsafe-type-assertion`, not evaluated — so a rule shared from a package is not found: set\n`\"lintHint\": false` in `domaintrail.jsonc` then.\n\n<!-- topic: checks -->\n### Generated checks\n\nEvery invariant and requirement with an expression becomes a pure function in the context's module, translated 1:1\nwith the semantics of [Invariant expressions](#invariant-expressions): equality by value / all fields / `id` / tag and\nfields, `exists` and `implies` as written, regular expressions as JavaScript (`matches` is `RegExp.test`), `length` as\nthe string's `length`, Decimals (carried as strings) and Ints compared as numbers, Timestamps by time.\n\n| Generated | For | Returns |\n|---|---|---|\n| `check<Name>(value)` | a domain primitive with invariants | the violated invariants, in declaration order (empty: valid) |\n| `make<Name>` | every domain primitive and ID | implements `Make<Name>`: `Invalid<Name>` with the **first** violated invariant |\n| `check<Name>(x)` | a value object, entity or union with invariants (a union: those of its variant) | the violated invariants |\n| `check<State><Aggregate>(x)`, `check<Aggregate>(x)` | an aggregate with invariants | per state, the aggregate's and the state's violated invariants; `check<Aggregate>` for any state |\n| `check<Command>Requires(aggregate, …params)` | a command with requirements | the name of the first requirement that does not hold, or `undefined` |\n| `equal<Name>(a, b)` | every value object, entity and union | the equality of `==`, `unique` and `contains` |\n\n```ts\n/** The first requirement of Order.confirm that does not hold, in declaration order; `undefined` when all hold. */\nexport function checkConfirmOrderRequires(order: DraftOrder, _at: Timestamp): 'hasItems' | undefined {\n if (!(order.items.length > 0)) return 'hasItems';\n return undefined;\n}\n```\n\nA constraint without an expression cannot be generated. Instead the check gets a **slot**: a type naming the\nfunction your code provides, and a `…With` variant that takes it and combines it with the generated parts, in\ndeclaration order. Generated code never imports your code; you pass the function in:\n\n```ts\n// generated (Sales.ts), for `invariant reserved \"予約語でない\"` in `primitive Sku`\nexport type SkuDeclaredChecks = { readonly reserved: (value: string) => boolean };\nexport function checkSkuWith(checks: SkuDeclaredChecks): (value: string) => readonly InvalidSku['invariant'][];\nexport const makeSkuWith: (checks: SkuDeclaredChecks) => MakeSku;\n\n// your code\nexport const makeSku = Sales.makeSkuWith({ reserved: (value) => !RESERVED.has(value) });\n```\n\nThe same holds for the other checks: `<Type>DeclaredChecks` and `check<Type>With(checks)` for value types and\naggregates (one object per aggregate, keyed by invariant name, for all its states), and\n`<Command>DeclaredRequires` and `check<Command>RequiresWith(checks)` for requirements. A type without such a\nconstraint has only the plain function.\n\n<!-- topic: tests -->\n### Generated tests\n\nWith `generate.ts.tests` in the config, `dt generate ts` also writes **contract tests** into `<out>/tests` (or\n`generate.ts.tests.out`). They check what the types cannot — the values — and use the generated checks as the oracle:\n\n| | Checks |\n|---|---|\n| T1 | `make<Name>` accepts exactly the values that hold the invariants, with values at and next to each bound of ranges and lengths and strings for regular expressions, and reports the first one violated |\n| T2 | when a requirement does not hold, the command returns the `else` error of the first such requirement; when all hold, none of the `else` errors |\n| T3 | after a successful command, the aggregate holds the invariants of its state (the aggregate's and the state's) |\n| T4 | events whose bounds the types do not express (`X[2..5]`) are emitted that many times |\n| T5 | purity: the same input twice gives deep-equal results, and the input is not changed |\n| T6 | with `required` policies: for every event, the handler of a policy resolves to an input the command accepts — every argument present unless optional, of its type (the fields of value objects and entities in turn), holding the invariants of its type, a list within its multiplicity |\n\nInputs are generated with [fast-check](https://fast-check.dev) from the types: IDs, ranges, lengths and regular\nexpressions of domain primitives, enums, value objects, entities, unions, collections with their multiplicities, and\naggregates per state. A parameter of a verified type gets values of its base type, taken as verified (the check\npassed); `arbitraries` overrides it like any other type (`{ Sales: { AvailableEmail: … } }`). **Inputs are always valid**: only values that hold their invariants are used, and parameters\nare values of their types; what a command does with invalid input is outside its contract. Values of one type are\ndrawn often from a small pool, so that duplicates, `contains` and `!=` are decided both ways. Runs are deterministic\n(fixed seed, configurable), and a failure shows the shrunk counterexample and the seed. Install fast-check as a dev\ndependency of your project; generated product code does not use it.\n\nPer context, `tests/<Context>.ts` exports `contract(impl, options?)`, returning a suite of named test cases\n(`{ name, children }` of `{ name, run, todo? }`) for any runner, and `arbitraries(options?)`. `impl` lists what to\ntest, each optional: the constructors (`make: { Quantity: makeQuantity }`) and the commands per aggregate\n(`Order: orderCommands`); for a context whose policies are `required`, also `policies` (its `<Context>PolicyHandlers`),\nwhich is not optional. The handlers run as your code wires them, with whatever data they read: give them what the\ngenerated events need (below, a confirmed order for any ID). With `generate.ts.tests.runner` (`bun`, `vitest`, `jest`, `node`), `tests/_register.ts`\nregisters a suite with that runner, and the test file is a few lines:\n\n```ts\n// examples/shop/src/contract.test.ts\nimport { parseDecimal, type Sales as SalesTypes } from '../generated/domain/index.js';\nimport { Billing, Common, Sales, register } from '../generated/domain/tests/index.js';\nimport { billingPolicies } from './app/policies.js';\nimport { customerCommands } from './domain/customer-commands.js';\nimport { invoiceCommands } from './domain/invoice-commands.js';\nimport { orderCommands } from './domain/order-commands.js';\nimport { makeAmount, makeCustomerId, makeEmailAddress, makeInvoiceId, makeOrderId, makeOrderItemId, makeQuantity, makeSku } from './domain/primitives.js';\n\nregister(\n Sales.contract({\n make: { Quantity: makeQuantity, Sku: makeSku, CustomerId: makeCustomerId, OrderId: makeOrderId, OrderItemId: makeOrderItemId },\n Order: orderCommands,\n Customer: customerCommands,\n }),\n);\nconst ok = <T>(r: { ok: true; value: T } | { ok: false }): T => {\n if (!r.ok) throw new Error('invalid example');\n return r.value;\n};\n\n// The policy cases feed Billing's handlers generated `OrderConfirmed` events, so the order a handler reads comes from\n// here: a confirmed order with that ID. The tests run the handlers with whatever they are wired to.\nconst confirmed = (id: SalesTypes.OrderId): SalesTypes.ConfirmedOrder => ({\n state: 'Confirmed',\n id,\n customer: makeCustomerId('customer-1'),\n items: [\n {\n id: makeOrderItemId('item-1'),\n sku: ok(makeSku('ABC-0001')),\n quantity: ok(makeQuantity(3)),\n unitPrice: { amount: ok(makeAmount(parseDecimal('12.50')!)), currency: 'USD' },\n },\n ],\n confirmedAt: new Date('2026-10-24T09:00:00Z'),\n});\nlet invoices = 0;\nconst policies = billingPolicies({ loadOrder: async (id) => confirmed(id), newInvoiceId: () => makeInvoiceId(`invoice-${++invoices}`) });\n\nregister(Billing.contract({ make: { InvoiceId: makeInvoiceId }, Invoice: invoiceCommands, policies }));\nregister(Common.contract({ make: { EmailAddress: makeEmailAddress, Amount: makeAmount } }));\n```\n\nOptions, all optional:\n\n- `seed`, `runs`: fast-check's seed (default 42) and the number of generated inputs per case (default 100).\n- `arbitraries: { Sales: { Sku: fc.stringMatching(/^ABC-\\d{4}$/) } }`: replaces the arbitrary of a type, by context\n and type name (aggregates per state, e.g. `DraftOrder`). Its values are still filtered by the invariants. When\n filtering rejects 1000 values in a row, the case fails with a message naming the type to override.\n- `declared: { Sales: { Sku: { reserved: … }, Order: { … }, AcceptReview: { signedOff: … } } }`: the checks of the\n constraints without an expression, by context and type or command type; the same functions your code passes to the\n `…With` checks. A constraint whose check is not passed is listed as a **todo** case (\"not expressed in the DSL and\n not checked\"), and the tests accept either outcome where it would decide.\n- `examples: { Order: { states: { Draft: [order] }, commands: { confirm: [[order, at]] } } }`: valid aggregates per\n state (mixed into the generated ones) and arguments per command (run first, before the generated ones). Examples\n must hold their invariants; one that does not fails the case.\n\nA failing case says what was expected, e.g. `Order.cancel(…) made an aggregate that violates 'otherNeedsNote'`. Change\nthe implementation — or, if the implementation is right, the DSL (here a missing `requires` on `cancel`).\n\n**Outside the generated tests** is what happens around the commands, which your own tests cover (as\n`examples/shop/src/app.test.ts` does): the factories of verified types (`MakeAvailableEmail`, with your repositories and\nservices), the constraints without an expression where you pass no check, and the application — loading, saving,\npublishing events.\n\n<!-- topic: metadata -->\n### Technical metadata\n\nTechnical fields (see [Domain fields and technical fields](#domain-fields-and-technical-fields)) are added by your code,\naround the generated types. Keep the two apart in an envelope, `{ event, meta }`, rather than an intersection\n(`E & Meta`): names cannot clash, and domain and technical fields stay separate. `dt` generates no envelope type.\n\n```ts\n// src/stored.ts — your code: metadata of stored events and aggregates\nimport type { Sales } from '../generated/domain/index.js';\n\nexport interface EventMeta {\n readonly eventId: string;\n readonly recordedAt: Date;\n readonly correlationId?: string;\n readonly causationId?: string;\n}\n\n/** What Sales records in addition. */\nexport interface SalesEventMeta extends EventMeta {\n readonly accountId: string;\n}\n\nexport type Stored<E, M extends EventMeta = EventMeta> = { readonly event: E; readonly meta: M };\n\nexport type StoredSalesEvent = Stored<Sales.PersistentEvent, SalesEventMeta>;\n\nexport interface AggregateMeta {\n readonly version: number;\n readonly createdAt: Date;\n readonly updatedAt: Date;\n}\n\nexport type Versioned<A, M extends AggregateMeta = AggregateMeta> = { readonly aggregate: A; readonly meta: M };\n\nexport interface OrderRepository {\n load(id: Sales.OrderId): Promise<Versioned<Sales.Order> | undefined>;\n save(order: Versioned<Sales.Order>, events: readonly Sales.OrderEvent[]): Promise<void>;\n}\n```\n\nMetadata that only some kinds of event carry can be keyed by the event's `type`:\n\n```ts\n// src/meta-by-event.ts — your code\nimport type { Sales } from '../generated/domain/index.js';\nimport type { EventMeta } from './stored.js';\n\ninterface MetaByEvent {\n OrderPlaced: { readonly channel: 'web' | 'store' };\n}\n\nexport type MetaOf<E extends { type: string }> = EventMeta & (E['type'] extends keyof MetaByEvent ? MetaByEvent[E['type']] : {});\n\n/** One envelope per kind of event, so that a union of events gives a union of envelopes. */\nexport type StoredEvent<E extends { type: string }> = E extends unknown ? { readonly event: E; readonly meta: MetaOf<E> } : never;\n\nexport const channelOf = (s: StoredEvent<Sales.OrderPlaced>): 'web' | 'store' => s.meta.channel;\nexport type StoredSalesEvents = StoredEvent<Sales.DomainEvent>;\n```\n\n<!-- topic: output-dirs -->\n### Output directories\n\nBy default every context goes to `generate.ts.out`. Contexts listed in `generate.ts.contexts` go to their own\ndirectory instead, e.g. a shared context into a package used by several applications. Each directory written to gets\nits own `_runtime.ts` and an `index.ts` re-exporting only the contexts in it; a module imports other contexts by\nrelative path (`import type * as Common from '../../packages/shared/src/generated/Common.js'`). The runtimes are\nidentical, so values typed with one are accepted where the other is expected.\n\nEach run replaces the files generated before (recognized by their header). The files written are recorded in\n`.domaintrail/generated.json` under the root, and generated files of earlier runs that are no longer written (an output\ndirectory moved, a context removed) are deleted, along with a directory left empty. Only recorded paths inside the root\nand not reached through a symlink are deleted; other entries in that file are ignored. Other files are never deleted —\nneither hand-written files nor generated files you edited — and generation stops instead of overwriting one.\n<!-- topic: rules -->\n## Rules\n\nEach diagnostic names its rule. A `hint` says what to change when the message does not; unresolved names get\n`did you mean …?` instead.\n\n| Code | Check |\n|---|---|\n| SYNTAX | lexer / parser errors |\n| M001 | types, aggregates and names in invariants and requirements resolve |\n| M002 | names are unique in their scope (also across files of one context) |\n| M003 | an entity is owned by exactly one aggregate (warning when owned by none) |\n| M004 | no ownership across aggregate boundaries (entities need `owns`, aggregates need `references`, …) |\n| M005 | a reference's ID type matches the identity of the target aggregate |\n| M006 | command transitions: states exist; stateful: `create` has one `-> X`, others one or more `A -> B` with sources; a state is the source of one transition per command; stateless: none. `emits … when`: its states are sources / targets of the command, it matches an outcome, a factory selects by target only, stateless: no `when` |\n| M007 | `emits` / `errors` / `else` refer to existing events / errors |\n| M008 | an invariant or requirement is a Bool expression |\n| M009 | operators and operands have compatible types; a domain primitive compares with the same one, literals, and (over `Int` / `Decimal`) `count(…)` / `length(…)` |\n| M010 | state-specific fields are used only in their own state's invariants, and in requirements of commands whose every source state has them |\n| M011 | generated TypeScript names do not collide, including the events of one command (keyed by name) |\n| M012 | the ownership graph has no cycles |\n| M013 | aggregates and entities have an `id` field of their own ID type |\n| M014 | shared contexts: only value types, no dependency on other contexts, consistent `shared`, unambiguous names (shadowing is a warning) |\n| M015 | unions: at least one variant, variants match the bound enum exactly, no variant field named like the discriminator |\n| M016 | field sets: plain fields only (no `id`), `with` only on aggregates, entities and events, each field name from one place |\n| M017 | (warning) an event emitted by an aggregate's command has a field of that aggregate's ID type |\n| M018 | an invariant or requirement without an expression has a label or a `///` description |\n| M019 | a parameter of a command with `requires` is not named like a field its requirements see |\n| M020 | (warning) a `create command` has a parameter of the aggregate's ID type (commands are pure: the ID is passed in) |\n| M021 | `changes` names fields the command can change: fields of the aggregate other than its identity, a state-specific one carried over by an outcome; none on a `create command` |\n| M022 | (warning) a field of an event or error a command emits or returns, with no source of the same name and type (parameters, their fields, the aggregate's fields), while some source has its type; ID-typed, Int, Decimal and Bool fields are exempt |\n| M023 | policies: the event exists, the aggregate is one of the policy's own context, the command is one of the aggregate, the event is not of a shared context; (warning) two policies of a context state the same reaction |\n| M024 | verified types: the base and the context of `for` are value types (`for` not a verified type), no cycle; a verified type types command parameters and other verified types only, no field |\n| L001 | (warning) an inline field label disagrees with the term, or context fragments disagree on the context name |\n| L002 | (warning) a term is not used by any field or parameter |\n| L003 | (warning) different things in one scope share a Japanese name |\n\n<!-- topic: show -->\n## Looking up one element\n\nTo understand one element of a large model, run `dt show <name>` instead of reading the `.dt` files: it prints the\nelement resolved, close to the DSL — the fields of field sets inline (`// with Common.Tenanted`), each field's label\nwith where it comes from (`(from term)`, `(from type)`; none when written inline), multiplicities, `owns` /\n`references -> X`, invariants with their expressions (or `(not expressed in the DSL)`), states with their own fields\nand invariants, commands with their transitions, `requires … else`, `changes`, the events of each outcome (`when`\nresolved), the full error union, a verified type's `of`, `for` and `errors`, a `// file:line` location per declaration\n(relative to the root), and who uses it (owns, references, its type or ID, emits, returns, `with`, verified types).\n\n```bash\ndt show # the contexts and their elements\ndt show Sales # a context: terms and one line per element\ndt show Sales.Order # an element (Order alone is enough when no other context has one)\ndt show Sales.Order.confirm # a command: its contract, the fields each outcome sets / carries / drops, payloads\ndt show Sales.Order.Confirmed # a state: its fields, invariants, the commands that enter and leave it\n```\n\nA name may leave out leading parts (`Order`, `Order.confirm`) when it names one thing only; when it names several, they\nare listed (exit 1); an unknown name gets `did you mean …?` (exit 1). It works on a model with semantic errors as far\nas it resolves (unresolved types are marked `(unresolved)`, and a note on stderr gives the number of errors): the exit\ncode is 0 whenever the name was found, whatever the diagnostics. A model with syntax errors cannot be shown (exit 1).\n\n`--format json` prints one JSON document, the Typed Model of what was found, keyed by `show`:\n\n- `{\"show\": \"element\", \"element\": {...}, \"usedBy\": {...}}`: the element (`id`, `kind`, `fields` with `label`,\n `labelSource`, `multiplicity`, `fieldSet`, …, `invariants`, `states`, `commands`, `location`); each command has\n `outcomes`: `[{\"from\", \"to\", \"emits\": [{\"event\", \"multiplicity\", \"lower\", \"upper\"}], \"sets\", \"carries\", \"drops\"}]`.\n A verified type has `verified`: `{\"of\", \"for\": {\"type\", \"optional\"}, \"errors\"}`; a policy has `policy`:\n `{\"event\", \"aggregate\", \"command\"}`. `usedBy` has the lists `takenOnBy`, `ownedBy`, `typeOf`, `referencedBy`,\n `idUsedBy`, `emittedBy`, `returnedBy` (commands, and verified types whose factory returns the error), `verifiedAs`\n (verified types of it), `contextOf` (verified types whose `for` it is), `discriminatorOf` and `reactedToBy` (the\n policies that react to the event), as dotted paths (`Billing.Invoice.order`, `Sales.Order.addItem(item)`). Each\n command has `triggeredBy`: the policies that run it.\n- `{\"show\": \"command\", \"aggregate\", \"command\"}` (with `outcomes` and `triggeredBy`), `{\"show\": \"state\", \"aggregate\", \"state\", \"enteredBy\",\n \"leftBy\"}`, `{\"show\": \"context\", \"context\"}` and `{\"show\": \"overview\", \"contexts\"}` (name, label, doc, shared, terms\n and the elements with their location).\n- `\"modelErrors\": n` when the model has errors; `{\"error\": \"...\", \"candidates\": [...]}` when the name is unknown\n (the suggestions) or ambiguous (the matches).\n\n<!-- topic: fmt -->\n## Formatting\n\n`dt fmt` rewrites every file of the workspace (the config's `sources`, like `check`) in the one canonical layout;\n`dt fmt <files…>` formats only those files (paths relative to the current directory). Run it after every edit, so that\nthe same model is always written the same way and a diff shows only what changed. `dt fmt --check` writes nothing,\nlists the files that are not formatted and exits 1 if there is one (for CI). A file with syntax errors is reported\nlike `dt check` does and left untouched (exit 1). The language server formats a document the same way.\n\nFormatting changes only whitespace and three spellings: `[0..1]` becomes `?`, `[1..1]` is removed, and the commas\nbetween enum values or between transitions are dropped. The model never changes, strings (regular expressions\nincluded) are untouched, and every comment stays in its place: on the line of the code before it, or on a line of its\nown. The layout:\n\n- 2 spaces of indentation per level. Every member of a block (`context`, `aggregate`, `state`, `value`, `event`, `terms`,\n …) is on its own line; a block without members is `{}`. One blank line between members is kept where the author wrote\n one or more, none after `{` or before `}`. Contexts in one file are separated by one blank line.\n- An enum stays on one line (`enum Currency { JPY \"日本円\" USD \"米ドル\" }`), unless it has comments or would be longer\n than 120 columns (a Japanese character counts as two); then one value per line.\n- An expression body stays inline (`invariant positive { amount > 0 }`), unless it has comments or its line would be\n longer than 120 columns; then the expression goes on a line of its own between `{` and `}`. An expression is never\n broken.\n- A command with at most one clause — a transition, `requires`, `changes`, `emits` with one event, or `errors` — and\n no comments stays on one line if it fits in 120 columns: `command start() Draft -> InProgress`. Otherwise each clause\n goes on a line of its own, one level deeper, in the order of the grammar, and a list of several events after `emits`\n takes one line per event, aligned after `emits `:\n\n ```text\n command cancel(reason: CancelReason)\n Draft | Confirmed -> Cancelled\n emits OrderCancelled,\n RefundRequested when Confirmed ->\n ```\n\n- Parameters, `with`, `changes` and `errors` lists stay on one line. One space after `:` and `,` and around `->`, `|`,\n `=>` and operators; none inside `( )`, `[ ]`, around `.` and `..`, or before `?`. One space before a comment at the\n end of a line.\n\n<!-- topic: output -->\n## Output and exit codes\n\n`check` (and `generate ts`, which checks first) prints one line per problem, sorted by file and position, then a\nsummary. When the message does not say how to fix the problem, an indented `hint:` line follows: what to change, or\n`did you mean …?` with the closest names of the right kind visible there (at most 3) for a name that does not resolve.\n\n```text\nsales/order.dt:2:11 - warning L002: Term 'notes' (\"備考\") is not used by any field or parameter in context 'Sales'.\n hint: did you mean 'note'?\nsales/order.dt:8:32 - error M009: count() requires a collection, but got 'Money'.\nsales/order.dt:12:30 - error M006: Cannot resolve state 'Confirmd' in this aggregate.\n hint: did you mean 'Confirmed'?\n1 file(s) checked: 2 error(s), 1 warning(s).\n```\n\nPaths are relative to the root, lines and columns 1-based. Errors (and their hints) go to stderr, warnings to stdout.\nFix errors first; warnings do not fail the check. Other failures (missing root, invalid config, port in use, …) are one\nline on stderr.\n\nWith `--format json` (`check` and `generate ts`), the output is one JSON document on stdout instead, with the same\norder and exit codes (JSON Schema: `schema/diagnostics.schema.json` in the package):\n\n```json\n{\n \"diagnostics\": [\n { \"file\": \"sales/order.dt\", \"line\": 12, \"column\": 30, \"endLine\": 12, \"endColumn\": 38, \"severity\": \"error\",\n \"code\": \"M006\", \"message\": \"Cannot resolve state 'Confirmd' in this aggregate.\", \"hint\": \"did you mean 'Confirmed'?\" }\n ],\n \"summary\": { \"files\": 6, \"errors\": 1, \"warnings\": 0 }\n}\n```\n\n`endLine` / `endColumn` point just past the problem; `hint` is present only when there is one. `generate ts` adds\n`written` (the output directories). A failure other than the diagnostics (missing root, invalid config, no `.dt` file,\ngeneration refused) is an `error` message in the document, e.g. `{ \"error\": \"Root directory not found: domain\" }`;\n`diagnostics` and `summary` are there whenever the check ran.\n\n| Exit code | Meaning |\n|---|---|\n| 0 | success (`check`: no errors; warnings are allowed) |\n| 1 | the model has errors, no `.dt` file was found, generation was refused, `show` did not find the name (or it is ambiguous), `guide` got an unknown topic, or the root / a file could not be read |\n| 2 | invalid configuration (`domaintrail.jsonc` / `--config`, including a context name in `generate.ts.contexts` that the model does not have) or an unknown `generate` target |\n\n<!-- topic: config -->\n## Configuration\n\n`domaintrail.jsonc` (comments and trailing commas allowed) or `domaintrail.json` is looked up in `--root` (`-r`,\ndefault `.`) and its parents, like `tsconfig.json`; `--config <file>` (`-c`) picks one explicitly. All paths in it are\nrelative to the config file, and its directory becomes the root. Without a config file, the defaults apply with\n`--root` as the root. Every key is optional.\n\n```jsonc\n{\n \"$schema\": \"./node_modules/domaintrail/schema/domaintrail.schema.json\",\n // globs of DSL files; \"!\" excludes (node_modules, .git, .domaintrail are never scanned)\n \"sources\": [\"domain/**/*.dt\", \"!domain/drafts/**\"], // default: [\"**/*.dt\"]\n // skip what .gitignore ignores when finding sources\n \"respectGitignore\": true, // default: true\n // print a hint on generate when no lint rule against casts to domain types is found (dt guide lint)\n \"lintHint\": true, // default: true\n \"generate\": {\n \"ts\": {\n \"out\": \"src/domain/generated\", // default: \"generated/domain\"\n // policies: \"optional\" (a handler type each) or \"required\" (also <Context>PolicyHandlers, which the contract tests take)\n \"policies\": \"optional\", // default: \"optional\"\n // per context: a directory of its own instead of \"out\", and/or its own \"policies\" (both optional)\n \"contexts\": { \"Common\": { \"out\": \"packages/shared/src/generated\" }, \"Billing\": { \"policies\": \"required\" } },\n // contract tests, written only with this key (optional); needs fast-check in your project\n \"tests\": { \"out\": \"src/domain/generated/tests\", \"runner\": \"vitest\" } // default out: <out>/tests; runner: bun, vitest, jest, node\n }\n },\n \"view\": { \"port\": 4321, \"host\": \"127.0.0.1\" } // defaults shown\n}\n```\n\nCLI options win over the file: `generate ts --out`, `view --port`, `view --host`. `--out` replaces `generate.ts.out`\nonly; contexts listed in `generate.ts.contexts` keep their own directories. Sources and output directories, `--out`\nincluded, must stay inside the config file's directory (the root without a config file): an absolute path or one with\n`..` is a config error, and an output directory reached through a symlink stops generation. A name in\n`generate.ts.contexts` that is not a context of the model is a config error, reported by `generate ts` once the model\nhas been checked. Without `generate.ts.tests.out`, the tests follow `--out` (`<out>/tests`). The policies of a context\nare `generate.ts.contexts.<Context>.policies` if given, else `generate.ts.policies`, else `optional` (see\n[Reactions to events](#reactions-to-events-policies)).\n\n**Sources and `.gitignore`.** The sources are the files below the root that match `sources`, outside `node_modules`,\n`.git` and `.domaintrail`, and not ignored by git: every command that finds them (`check`, `generate`, `view`, `fmt`,\n`show`, `lsp`) skips the files and directories that the `.gitignore` files ignore, with git's rules (`!` negation,\n`dir/` for directories only, a leading or inner `/` anchoring to the file's directory, a deeper file winning). The\n`.gitignore` files that count are those of the root and its subdirectories, those of the directories above the root up\nto the top of the enclosing git repository, and the repository's `.git/info/exclude`; without a repository, only those\nfrom the root down (a `.gitignore` counts whether or not the directory is a repository). A root that the rules above it\nignore (a copy of a project in an ignored directory, checked with `-r`) is still checked, with the `.gitignore` files\nfrom the root down only. So a generated or cloned copy of the model in an ignored directory (an editor's grammar\ncheckout, a build output) is not checked twice. `\"respectGitignore\": false` turns this off; files named on the command\nline (`dt fmt <files>`) are never skipped.\n\n<!-- topic: lsp -->\n## Language server\n\n`dt lsp` is a language server (LSP) for `.dt` files, speaking over stdin/stdout. Editors and AI agents start it; it\nruns until the client shuts it down. It offers:\n\n- **Diagnostics equal to `dt check`**: the same rules, severities, codes (the rule ID, e.g. `M009`) and messages,\n computed over the whole workspace (the config's `sources`) with the unsaved text of open files, after every change.\n A hint follows the message on a second line (`hint: did you mean 'Confirmed'?`) and is also in the diagnostic's\n `data.hint`.\n- **Go to definition** and **find references** for types, states, events, errors, field sets and the commands of\n policies, across files and contexts (`Sales.Order` included).\n- **Hover**: kind, name, Japanese name, `///` description, the expression of an invariant or requirement, the\n `emits` entries of a command and the policies that run it, the reaction of a policy, and the policies that react to\n an event.\n- **Outline** (document symbols), **completion** of names, and **rename** of context elements, states and commands\n (a command is referenced by policies), which updates every reference (a qualified `Sales.Order` keeps its context).\n Contexts, fields and parameters are not renamed, since their names also appear outside references (in qualified\n names, expressions and terms).\n\nThe workspace is the config found from `--root` or `--config` when given, else from the client's (first) workspace\nfolder, else from the current directory, exactly as `dt check` would find it there (with the same sources, `.gitignore`\nincluded). When that search finds no config file, the first `.dt` file opened below that directory whose directory or\na parent (up to it) has a `domaintrail.jsonc` / `domaintrail.json` makes that config the workspace: its directory is\nthe root, its `sources` are loaded, and the files no longer in the workspace lose their diagnostics. So opening a\nrepository with the model in `examples/shop/` checks that model as `dt -r examples/shop check` does. A session has one\nworkspace: a file opened later under another config file gets no diagnostics, and a warning says so once (open that\ndirectory as its own project to check it). An open file outside the workspace's sources gets no diagnostics either.\n\nA client needs only: command `dt lsp` (with `dt` on the `PATH`, or e.g. `npx dt lsp`), transport stdio, file\nextension `.dt`. For Claude Code, a plugin declares it in an `.lsp.json` at the plugin root (load it with\n`claude --plugin-dir <plugin directory>`); Claude then sees the diagnostics after each edit of a `.dt` file:\n\n```json\n{\n \"domaintrail\": {\n \"command\": \"dt\",\n \"args\": [\"lsp\"],\n \"extensionToLanguage\": { \".dt\": \"domaintrail\" }\n }\n}\n```\n\n<!-- topic: editors -->\n## Editors\n\n**Zed.** The extension in `editors/zed` of the repository highlights `.dt` files with the tree-sitter grammar in\n`tree-sitter-domaintrail`, gives brackets, indentation and an outline (contexts, their declarations, the states and\ncommands of aggregates), and starts `dt lsp` (diagnostics, navigation, hover, rename, formatting; see [Language\nserver](#language-server)). Install it with *zed: install dev extension* and the `editors/zed` directory of a clone\n(it needs Rust, installed with rustup). The language server is the first of: `lsp.domaintrail.binary` in Zed's\nsettings, `dt` on the `PATH`, else domaintrail at the version the extension pins, installed into the extension's own\ndirectory and run with Zed's Node. By default the extension runs no file of the project (such as its\n`node_modules/.bin/dt`); to use the project's own `dt`, name it in the settings. Zed merges a project's\n`.zed/settings.json` into these settings, so a project can name a command too, as for Zed's built-in language servers:\nopen untrusted repositories in Zed's restricted mode.\n\n```json\n{ \"lsp\": { \"domaintrail\": { \"binary\": { \"path\": \"/path/to/dt\", \"arguments\": [\"lsp\"] } } } }\n```\n\nOpen the directory with the `domaintrail.jsonc` as the project, or a directory above it: then the first `.dt` file\nopened picks the nearest config (see [Language server](#language-server)), provided no config is found from the\nproject directory itself. Directories that `.gitignore` ignores, such as the grammar checkout Zed keeps in\n`editors/zed/grammars` when the extension is installed from a clone, are not part of the workspace.\n\n**Other editors.** Any LSP client works with command `dt lsp`, transport stdio, file extension `.dt` (see\n[Language server](#language-server)). Editors that highlight with tree-sitter (Neovim, Helix, Emacs) can use the\ngrammar in `tree-sitter-domaintrail`.\n";
|
|
13876
|
+
var guide_default = "# DomainTrail guide\n\n`dt` checks domain models written in the DomainTrail DSL (`.dt` files) and generates TypeScript types and command\ncontracts from them. The DSL describes domain **structure and contracts** only — never business logic, which is written\nby hand against the generated types.\n\nThis guide is written for people and AI agents using `dt` without other documentation. `dt guide` prints the quick\nreference below and the list of topics, `dt guide <topic>` one section (the name in the `<!-- topic: … -->` line above\nits heading, e.g. `dt guide requires`), `dt guide <rule>` one rule (`dt guide M010`), and `dt guide --all` all of it.\n\n<!-- topic: quick -->\n## Quick reference\n\n**The loop.** Edit `.dt` files → `dt fmt` → `dt check` until it exits 0 → `dt generate ts` → implement each command\nwith `decide` against its generated `<Command><Aggregate>Decision` type → type-check your code (`tsc --noEmit`) → run\nthe generated contract tests. Never edit generated files; change the `.dt` sources and generate again.\n\n**The DSL at a glance** (one file of a valid workspace):\n\n```dt\n// library/loan.dt\ncontext Library \"図書館\" { // a bounded context; \"…\" is a label (表示名)\n id LoanId: UUID // an ID type\n id BookId: UUID\n value Period { // a value object\n days: Int\n }\n\n aggregate Loan \"貸出\" {\n id: LoanId // identity (required)\n book: BookId\n owns period: Period\n renewals: Int\n note: String? // ? = optional\n invariant renewalsLimited { renewals >= 0 and renewals <= 2 } // holds in every state\n\n state Lent\n state Overdue\n state Returned {\n returnedAt: Timestamp // only in this state\n }\n\n create command lend(id: LoanId, book: BookId, period: Period) \"貸し出す\"\n -> Lent\n emits BookLent\n command renew(extension: Period)\n Lent -> Lent\n requires underLimit { renewals < 2 } else RenewalLimitReached\n changes period, renewals\n emits LoanRenewed\n command markOverdue() Lent -> Overdue\n command giveBack(at: Timestamp)\n Lent | Overdue -> Returned\n emits BookReturned,\n FeeCharged? when Overdue ->\n errors BookDamaged\n }\n\n event BookLent {\n loanId: LoanId\n book: BookId\n }\n event LoanRenewed {\n loanId: LoanId\n period: Period\n }\n event BookReturned {\n loanId: LoanId\n returnedAt: Timestamp\n }\n event FeeCharged {\n loanId: LoanId\n days: Int\n }\n error RenewalLimitReached\n error BookDamaged\n}\n```\n\n**The contract keywords:**\n\n- `owns x: T` — an entity or value held by the aggregate; `references x: XId -> X` — another aggregate, by ID only.\n- `invariant name { expr }` — holds whenever the aggregate (or, inside a `state`, that state) exists.\n- `state S { … }` — a lifecycle state with its own fields; without states, commands have no transitions.\n- `create command c(…) -> S` — makes a new aggregate; it takes the new ID as a parameter (commands are pure).\n- `A | B -> C | D` — the command runs in `A` or `B` and ends in `C` or `D`; one transition per source state.\n- `requires name { expr } else E` — the command is accepted only if; otherwise it returns `E` (the first one failing).\n- A rule over a changeable policy: an invariant over a policy kept on the aggregate, or `requires` (`dt guide policies`).\n- `changes a, b` — the fields it may change; without `changes`, no common field (a target state's new fields are set).\n- `emits X` — exactly one `X` on success; `X?` at most one, `X[1..*]` one or more; `X when A -> B` only then.\n- `errors E` — errors it may return besides those of `else`; on an error it emits no event.\n- `verified V of T for C? errors E` — evidence of a check outside the aggregate, as a parameter (`dt guide verified`).\n- `policy P when E -> A.c` — when event `E` happens, command `A.c` eventually runs (`dt guide reactions`).\n\n**The commands:**\n\n- `dt guide [topic]` — this reference, one section, or `--all`.\n- `dt fmt` — format the `.dt` files in place (`--check` for CI).\n- `dt check` — check the model; exit 0 when there are no errors (`--format json` for one JSON document).\n- `dt generate ts` — check, then write the TypeScript (and the contract tests, if configured).\n- `dt show <name>` — one element, command or state resolved, with who uses it; read it instead of the `.dt` files.\n- `dt lsp` — language server over stdio: the diagnostics of `dt check`, definitions, references, hover, rename.\n- `dt view` — the Live View in the browser; it runs until Ctrl+C, so run it in the background.\n\n**When the contract and the implementation disagree**, follow `@see` on the generated type to the declaration and\nchange the DSL, not the implementation: never emit an event or set a field only to satisfy the types.\n\n**Never cast to a domain type** (`as X`): make it with `make<Name>`, `parseDecimal` or a factory (`dt guide lint`).\n\n**After upgrading domaintrail**, follow the Upgrading steps in `node_modules/domaintrail/CHANGELOG.md`.\n\n<!-- topic: workflow -->\n## Workflow\n\n1. Write or edit `.dt` files (see [DSL](#dsl) and [File layout](#file-layout-recommended)).\n2. Run `dt fmt` to put them in the canonical layout (see [Formatting](#formatting)).\n3. Run `dt check` and fix every reported problem until it exits 0.\n4. Run `dt generate ts` to write the TypeScript (it runs `check` first and writes nothing on errors).\n5. Implement the generated contracts in your own code — each command as a decision with `decide` (see\n [Decisions and `decide`](#decisions-and-decide)) — and type-check that code with your own TypeScript setup\n (e.g. `tsc --noEmit`). `dt` does not run the TypeScript compiler.\n6. With `generate.ts.tests` configured, run the generated contract tests against your implementation with your test\n runner (see [Generated tests](#generated-tests)).\n\n`dt view` starts the Live View, a browser page where people watch the model update as files are saved. It is a server\nthat keeps running and never exits by itself (stop it with Ctrl+C), so do not wait for it to finish: run it in the\nbackground, or use `dt check` for a result.\n\n`dt --help` and `dt <command> --help` list the options with examples.\n\n```bash\ndt fmt # format every *.dt file below the current directory in place\ndt check # every *.dt file below the current directory\ndt -r domain check # below domain/ (or as configured by a domaintrail.jsonc found there or above)\ndt generate ts # into <root>/generated/domain\ndt generate ts --out src/domain/generated\ndt view --port 8080 # http://127.0.0.1:8080, until Ctrl+C\n```\n\n<!-- topic: layout -->\n## File layout (recommended)\n\nFile names and directories carry no meaning: every matching `.dt` file belongs to the workspace, and a context may be\nsplit across any files. The recommended layout is one directory per context and one file per aggregate:\n\n```text\ncommon/context.dt shared context Common \"共通\" { ... } // a shared context has no aggregates\nsales/context.dt /// 説明 context Sales \"販売\" { terms { ... } }\nsales/order.dt context Sales { id OrderId ... aggregate Order { ... } entity OrderItem ... event OrderPlaced ... }\nsales/customer.dt context Sales { id CustomerId ... aggregate Customer { ... } }\nbilling/context.dt /// 説明 context Billing \"請求\" { terms { ... } }\nbilling/invoice.dt context Billing { id InvoiceId ... aggregate Invoice { ... } }\n```\n\n- `<context>/<aggregate>.dt` holds the aggregate and what only it uses: its ID, entities, events, errors, enums,\n primitives and values.\n- `<context>/context.dt` holds the context's name (表示名), description and `terms`, and the elements used by several of\n its aggregates or by none (field sets, for example). The other files open the context without a name: `context Sales {\n ... }`.\n- An aggregate's ID stays in the aggregate's file even when other aggregates reference it.\n- Directory and file names are kebab-case: `PurchaseOrder` → `purchase-order.dt`, `OrderFulfillment` →\n `order-fulfillment/`.\n\nThis is a convention, not a rule: `check` neither requires nor warns about any layout.\n\n<!-- topic: dsl -->\n## DSL\n\nThe examples below are the files of one valid workspace (the file name is in the first comment).\n\n<!-- topic: aggregates -->\n### Contexts and aggregates\n\n```dt\n// sales/order.dt\ncontext Sales { // a bounded context; may be split across files\n id OrderId: UUID // ID type with a base primitive type\n id OrderItemId: UUID\n enum CancelReason { Requested Other } // values separated by spaces (commas are allowed; `dt fmt` drops them)\n value Address {\n city: String\n street: String\n }\n\n aggregate Order {\n id: OrderId // identity field of its own ID type (required)\n owns items: OrderItem[0..*] // owned entity / value inside the aggregate\n references customer: CustomerId -> Customer // another aggregate, by ID\n owns shipTo: Address? // ? = optional\n note: String?\n\n invariant quantitiesPositive { // applies to every state; sees the common fields\n all(items, item => item.quantity > 0)\n }\n\n create command place(id: OrderId, customer: CustomerId)\n -> Draft\n emits OrderPlaced // factory: `-> State`, no source\n\n state Draft\n state Confirmed {\n confirmedAt: Timestamp // exists only in this state\n invariant hasItems { count(items) > 0 and count(items) <= 100 } // sees common + Confirmed fields\n }\n state Cancelled {\n reason: CancelReason\n invariant otherNeedsNote { reason == CancelReason.Other implies exists(note) }\n }\n\n command confirm(at: Timestamp)\n Draft -> Confirmed // a transition: source state(s) -> target state(s)\n requires hasItems { count(items) > 0 } else EmptyOrder // accepted only if; otherwise returns EmptyOrder\n emits OrderConfirmed // exactly one OrderConfirmed whenever it succeeds\n\n command cancel(reason: CancelReason)\n Draft | Confirmed -> Cancelled // from either\n emits OrderCancelled,\n RefundRequested when Confirmed -> // only when it comes from Confirmed\n\n command changeNote(note: String?) // one transition per input state: the output depends on the input\n Draft -> Draft\n Confirmed -> Confirmed // one transition per line (`Draft -> Draft, Confirmed -> Confirmed` is allowed too)\n changes note // the fields it may change; without `changes`, no common field\n }\n\n entity OrderItem {\n id: OrderItemId\n quantity: Quantity\n owns unitPrice: Money\n }\n\n persistent event OrderPlaced { // storing it is part of its contract\n orderId: OrderId\n }\n persistent event OrderConfirmed {\n orderId: OrderId\n confirmedAt: Timestamp\n }\n event OrderCancelled { // not persisted (default)\n orderId: OrderId\n reason: CancelReason\n }\n event RefundRequested {\n orderId: OrderId\n }\n error EmptyOrder // payload optional: error E { field: Type }\n}\n```\n\n```dt\n// sales/customer.dt\ncontext Sales {\n id CustomerId: UUID\n /// 他の顧客に使われていないことを、登録・変更のときに確かめたメールアドレス。\n verified AvailableEmail \"未使用のメールアドレス\" of EmailAddress for CustomerId? errors EmailTaken // dt guide verified\n\n aggregate Customer { // no states: commands have no transitions\n id: CustomerId\n email: EmailAddress // from the shared context Common\n active: Bool\n owns addresses: Address[0..*]\n owns shipping: Address?\n\n invariant activeHasAddress { active == true implies count(addresses) > 0 }\n invariant servedArea { active == false or any(addresses, a => a.city == \"Tokyo\" or not (a.city == \"Osaka\")) }\n invariant distinctAddresses { unique(addresses, a => a.city, a.street) } // the combination is distinct\n invariant shippingListed { exists(shipping) implies contains(addresses, shipping) }\n\n /// No other customer has the same email address: checked when one is given (AvailableEmail), kept by the store.\n invariant uniqueEmail \"メールアドレスは顧客ごとに一意\"\n\n command changeEmail(email: AvailableEmail) // the check was made before: its error is the factory's\n changes email\n }\n\n error EmailTaken {\n email: EmailAddress\n }\n}\n```\n\n- Context elements: `id`, `primitive`, `enum`, `value`, `union`, `entity`, `aggregate`, `event`, `error`, `fields`\n (see [Field sets](#field-sets)), `verified` (see [Verified types](#verified-types)) and `policy` (see\n [Reactions to events](#reactions-to-events-policies)).\n- Fields: `name: Type`, `owns name: Type` (entities and values held by the aggregate),\n `references name: IdType -> Aggregate` (other aggregates, by ID only). Entities are owned by exactly one aggregate.\n- Primitive types: `String`, `Int`, `Decimal`, `Bool`, `Timestamp`, `UUID`.\n- Multiplicity after the type: `?` (also `[0..1]`, which `dt fmt` writes as `?`), `[0..*]`, `[1..*]`, `[n..m]`.\n- Aggregate members: fields, `invariant`, `state` (with its own fields and invariants), `command`.\n- A command is written in this order: parameters and label, transitions, `requires` (see\n [Command requirements](#command-requirements)), `changes` (see [Command changes](#command-changes)), `emits` (see\n [Command events](#command-events)), `errors`.\n- Transitions: `A | B -> C | D` means \"in `A` or `B`, the command ends in `C` or `D`\". Separate several transitions by\n line breaks or commas (`dt fmt` writes one per line). A state may be the source of one transition of a command only,\n so the input state decides the possible outputs; for several possible outcomes, list them on the right: `InProgress ->\n InProgress | Completed`.\n- In an aggregate with states, a `create command` makes a new aggregate and has exactly one transition without\n sources (`-> Draft`, or `-> A | B`); every other command has one or more transitions, each with sources. Without\n states, commands have no transitions. A command that keeps the state writes it out: `Draft -> Draft`.\n- `value`, `state`, `event` and `error` bodies are optional (`state Draft`, `error EmptyOrder`).\n- Other contexts' elements are referenced with qualified names: `Sales.OrderId`, `-> Sales.Order`.\n- Comments: `// …` and `/* … */`; `///` is a description (see [Names](#names-labels-and-descriptions)).\n\n<!-- topic: primitives -->\n### Domain primitives\n\nA single value with domain meaning and constraints, wrapping a primitive type. Values combining several fields are\nvalue objects.\n\n```dt\n// sales/primitives.dt\ncontext Sales {\n primitive Quantity \"数量\": Int { value in 1..999 } // one anonymous constraint\n primitive Sku: String { // or named invariants\n invariant format \"形式\" { matches(value, \"^[A-Z]{3}-\\d{4}$\") }\n invariant maxLength { length(value) <= 8 }\n }\n id InvoiceNo: String { matches(value, \"^INV-\\d{8}$\") } // an ID is a domain primitive used as identity\n}\n```\n\n- The base is always a primitive type; a domain primitive cannot refine another one or an enum.\n- Inside the constraint only `value` (of the base type) is visible; `length(value)`, `matches(value, \"regex\")` and\n ranges `value in 1..999` (negative literals allowed) are available there.\n- Regular expressions are written as in JavaScript: backslashes are not string escapes (`\"\\d\"` means `\\d`).\n- Elsewhere a domain primitive compares with literals, with the same domain primitive and, over `Int` or `Decimal`,\n with the numbers `count(…)` and `length(…)` compute (`count(approvedBy) >= policy.requiredApprovals`); not with\n another domain primitive or a plain field or parameter of its base type (M009).\n- Only IDs can be identity fields and reference types. Domain primitives cannot be owned.\n- The generated code includes the constructor: `makeQuantity` checks the invariants and returns the first one\n violated (`makeSkuWith(checks)` when an invariant has no expression); see [Generated checks](#generated-checks).\n\n<!-- topic: unions -->\n### Unions\n\nA value whose shape depends on a kind fixed when it is made (a tagged union). Use a union for a classification that\nnever changes, and aggregate states for a lifecycle that commands move through.\n\n```dt\n// sales/payment.dt\ncontext Sales {\n union Payer \"支払者\" { // discriminator `kind` by default\n CUSTOMER \"顧客\" {\n customerId: CustomerId\n }\n COMPANY \"法人\" {\n name: String\n billingEmail: EmailAddress?\n }\n }\n enum PaymentMethod { CARD TRANSFER }\n union PaymentDetails by method: PaymentMethod { // `by name`, optionally bound to an enum\n CARD { // exactly the enum's values (M015)\n last4: String\n }\n TRANSFER {\n bankCode: String\n }\n }\n}\n```\n\n- A union is a value type: usable like a value object (fields, `owns`, parameters, payloads, shared contexts).\n Variants hold value fields and invariants (seeing their own fields); entities and aggregates are not allowed.\n- In invariants, `payer.kind` compares with `Payer.COMPANY` (or the bound enum's values); variant fields are not\n accessible from outside the union.\n\n<!-- topic: shared -->\n### Shared contexts\n\n```dt\n// common/context.dt\nshared context Common \"共通\" { // shared kernel: usable by simple name from every context\n terms {\n currency: \"通貨\"\n }\n enum Currency { JPY USD }\n value Money {\n amount: Decimal\n currency: Currency\n }\n primitive EmailAddress: String { matches(value, \"^[^@\\s]+@[^@\\s]+$\") }\n}\n```\n\n- `Money` and `Common.Money` both work in other contexts. A context's own element with the same name wins (M014 warning).\n- A shared context holds only domain primitives, enums, value objects, unions, field sets and terms, and depends only on\n shared contexts. Its `terms` apply to fields of every context that has no term of its own for the name.\n\n<!-- topic: field-sets -->\n### Field sets\n\nFields that several aggregates, entities or events have for the same domain reason are written once as a field set\nand taken on with `with`.\n\n```dt\n// common/tenancy.dt\nshared context Common {\n /// 契約者(テナント)。業務データはテナントごとに分かれる。\n primitive TenantId \"テナントID\": UUID\n fields Tenanted \"テナント所属\" { // a field set: plain fields only\n tenant: TenantId \"テナント\"\n }\n}\n```\n\n```dt\n// expenses/expense.dt\ncontext Expenses \"経費\" {\n id ExpenseId \"経費申請ID\": UUID\n id StaffId \"職員ID\": UUID\n\n /// 規程により、申請の受付には実施者を記録する。\n fields Performed \"実施記録\" {\n performedBy: StaffId \"実施者\"\n }\n\n aggregate Expense \"経費申請\" with Tenanted { // tenant first, then its own fields\n id: ExpenseId\n applicant: StaffId \"申請者\"\n approver: StaffId? \"承認者\"\n invariant notSelfApproved \"申請者は承認できない\" { exists(approver) implies approver != applicant }\n\n create command submit(id: ExpenseId, applicant: StaffId \"申請者\", performedBy: StaffId \"実施者\") \"申請する\" // by proxy, too\n emits ExpenseSubmitted\n command approve(approver: StaffId \"承認者\", approvedAt: Timestamp \"承認日時\") \"承認する\"\n changes approver\n emits ExpenseApproved\n }\n\n event ExpenseSubmitted \"申請受付\" with Tenanted, Performed {\n expenseId: ExpenseId\n }\n event ExpenseApproved \"承認\" with Tenanted {\n expenseId: ExpenseId\n approver: StaffId \"承認者\" // a business role, named by the role\n approvedAt: Timestamp \"承認日時\" // when it happened in business terms\n }\n}\n```\n\n- `fields Name \"label\" { ... }` holds plain fields, written like any other field (type, multiplicity, label, `///`).\n No `owns`, `references`, invariants, `with` or `id` inside (M016).\n- `with A, B` after the name and label of an `aggregate`, `entity` or `event` (only those, M016). A field set is\n referenced like a type: by name in its own context and from shared contexts, as `Context.Name` otherwise.\n- The fields come first, in the order of `with`, and then count as the element's own: invariants see them, and an\n aggregate's states share them. A name may come from one place only, the element or one of its field sets (M016).\n- The identity of aggregates and entities is never a field set: every aggregate and entity declares `id` itself (M013).\n- A command's event should say which aggregate it is about: it needs a field of the aggregate's ID type, its own or\n from a field set (M017 warning). An event emitted by several aggregates needs the ID type of each (optional fields\n count).\n\n<!-- topic: fields -->\n### Domain fields and technical fields\n\nWrite a field in the DSL when **domain experts talk about it, or a business rule (an invariant, a transition, a\ncommand's decision) refers to it**. A field that is neither is technical: leave it out of the DSL and of the generated\ncode, and add it in your own code around the generated types (see [Technical metadata](#technical-metadata)).\n\n- **Required by the kind of element** (the ID of an aggregate or entity): enforced by the language (M013), not a field\n set.\n- **Chosen for some elements** (a tenant, a recorded performer): a field set and `with`.\n- **Technical**: event ID, the time the system recorded something (`recordedAt`), optimistic-lock `version`,\n `createdAt` / `updatedAt`, correlation / causation IDs, the system's acting account, schema versions.\n\nTwo cases look alike but are not:\n\n- **When**: the time something happened in business terms is domain — rules refer to it (\"within 30 days of\n confirmation\"). Name it by its meaning (`confirmedAt: Timestamp \"確定日時\"`). If every event is to carry its business\n time, say so with a field set: `fields Occurred { occurredAt: Timestamp \"発生日時\" }`. The time the system recorded it\n is technical. The two differ with late entry, offline work and imports.\n- **Who**: a business role (approver, person in charge) is domain — rules refer to it (\"the applicant cannot\n approve\"). Name it by the role (`approvedBy: StaffId \"承認者\"`), usually passed from the command's parameters to the\n event. If regulations require recording the performer of every operation,\n `fields Performed { performedBy: StaffId \"実施者\" }`. The login account, service account or IP address is\n technical; it differs with proxy entry and batches.\n\n<!-- topic: names -->\n### Names (labels and descriptions)\n\n```dt\n// billing/context.dt\n/// 注文への請求と支払いを扱う。\ncontext Billing \"請求\" {\n terms { // field / parameter names, defined once per context\n /// 請求書が支払われた日時。\n paidAt: \"支払日時\"\n }\n}\n```\n\n```dt\n// billing/invoice.dt\ncontext Billing { // other files open the context without a name\n id InvoiceId \"請求書ID\": UUID\n\n /// 注文1件に対する請求。\n aggregate Invoice \"請求書\" { // declarations: a string after the name\n id: InvoiceId\n references order: Sales.OrderId -> Sales.Order \"請求対象\" // an inline label on a field\n owns amount: Money \"請求金額\"\n state Open \"未払い\"\n state Paid \"支払済み\" {\n paidAt: Timestamp // → 支払日時 (from terms)\n }\n invariant positive \"金額が正\" { amount.amount > 0 }\n create command issue(id: InvoiceId, order: Sales.OrderId, amount: Money) \"請求書を発行する\" -> Open\n command pay(at: Timestamp \"支払日時\") \"支払う\"\n Open -> Paid\n emits InvoicePaid\n }\n enum InvoiceKind \"請求区分\" { Regular \"通常\" Refund \"返金\" }\n event InvoicePaid \"支払完了\" {\n invoiceId: InvoiceId\n paidAt: Timestamp\n }\n}\n```\n\n- Declarations (context, id, primitive, verified, enum and its values, value, union and its variants, entity,\n aggregate, state, command, invariant, event, error) take an optional string label after the name.\n- A field or parameter is named by, in order: its inline string → the context's `terms` → the name of its type\n (for `references`, the target aggregate) → the English name.\n- `///` lines directly above a declaration or term are its description.\n- Labels appear in the Live View and as JSDoc in the generated TypeScript; identifiers and generated type names stay\n English.\n\n<!-- topic: expressions -->\n### Invariant expressions\n\n- Literals: `true`, `false`, `42`, `-1`, `3.14`, `\"text\"`; enum values `CancelReason.Other`.\n- Field paths: `note`, `item.quantity`, `amount.amount`.\n- Operators, loosest first: `implies`, `or`, `and`, `== != < <= > >=`, `x in 1..9`, `not`.\n- Functions: `exists(x)` (an optional is present; narrows `x` on the right of `and` / `implies`), `count(c)`,\n `all(c, x => …)`, `any(c, x => …)`, `unique(c)`, `unique(c, x => key, …)`, `contains(c, x)`, and in domain\n primitives `length(value)`, `matches(value, \"regex\")`.\n- `unique(c)`: the elements of a collection are distinct. With keys, `unique(lines, l => l.sku)` means no two\n elements have the same key, and `unique(confirmations, c => c.scenario, c.reviewer)` that no two have the same\n combination of keys. A key cannot be optional (M009); to include an optional field, group the keys into a value\n object and use it as the key.\n- `contains(c, x)`: `x` is an element of `c`. A subset is written with `all`: `all(required, s => contains(confirmed, s))`.\n- Equality — `==`, `!=`, `unique` and `contains` compare alike: primitives and domain primitives by value, enum\n values by name, value objects by all their fields, entities by `id`, union values by discriminator and fields.\n Both sides must be of the same type (domain primitives also compare with literals, and over `Int` or `Decimal` with\n `count(…)` and `length(…)`, also in `<`, `<=`, `>`, `>=` and ranges); collections and optional values cannot be\n compared (guard an optional with `exists(...)` first).\n- An invariant must be a Bool expression. `dt check` checks only its type; whether a value holds it is checked by the\n generated check functions (`checkConfirmedOrder`, see [Generated checks](#generated-checks)), with exactly these\n semantics.\n- A constraint the DSL cannot express is declared without an expression, with a label or a `///` description that\n states it (M018): `invariant uniqueEmail \"メールアドレスは顧客ごとに一意\"`. It is documented like any invariant (in\n the Live View and as `@invariant uniqueEmail (…): not expressed in the DSL` in the generated TypeScript) and is\n checked by your code only: the generated checks take it as a function you pass in (a slot). In a domain primitive,\n its name joins the invariants of `Invalid<Name>`.\n- `count`, `exists`, `length`, `matches`, `all`, `any`, `unique` and `contains` are also usable as field and parameter\n names; followed by `(` they are the functions.\n\n<!-- topic: requires -->\n### Command requirements\n\n`requires` states when a command is accepted. Like an invariant it is a Bool expression, written after the\ntransitions, any number per command; `else` names the error the command returns when it does not hold:\n\n```dt\n// review/review.dt\ncontext Review {\n id ReviewId: UUID\n id PersonId: UUID\n value ApprovalPolicy {\n approvers: PersonId[1..*]\n }\n\n aggregate Review \"レビュー\" {\n id: ReviewId\n policy: ApprovalPolicy\n state Reviewing {\n openBlockers: Int\n }\n state Escalated {\n openBlockers: Int\n }\n state Accepted {\n acceptedBy: PersonId\n /// All the scenarios the policy requires are confirmed.\n invariant fullyConfirmed \"必要な確認がそろっている\" // holds whenever the review is Accepted\n }\n\n command accept(person: PersonId) \"受入\"\n Reviewing | Escalated -> Accepted\n requires authorizedApprover \"承認者である\" { // depends on the parameters and the current state\n contains(policy.approvers, person)\n } else NotApprover\n requires noOpenBlocker \"未解決の阻害がない\" { openBlockers == 0 } else Blocked\n /// The reviewer's own sign-off is recorded outside this model.\n requires signedOff \"本人の承認が記録済み\" else NotSignedOff // not expressed in the DSL\n errors Conflict\n }\n\n error NotApprover\n error Blocked\n error NotSignedOff\n error Conflict\n}\n```\n\n- **Invariant or requirement?** An invariant holds whenever the aggregate is in a state: it describes the state\n after a transition (an accepted review has all required confirmations). A requirement decides whether the command\n is accepted, from its parameters and the aggregate's current data (the person is among the approvers). If a\n condition reads \"after the command, …\", it is an invariant of the target state; if it reads \"the command may run\n only when …\", it is a requirement.\n- **Not for application-level authorization.** Whether the logged-in user has a role is checked by the application,\n not the model. Only when the policy is part of the aggregate's state (its approvers) is it a domain precondition.\n- **Scope**: the parameters; the aggregate's common fields (including those of field sets); and the fields that every\n source state of the command has, with the same name and type (with one source state, all its fields). A\n `create command` sees its parameters only, a command of an aggregate without states all its fields. Using another\n field is M010. Other aggregates cannot be referenced; pass what is needed as parameters.\n- A parameter may not be named like a field its command's requirements see (M019), so that a name in `requires` has\n one meaning. Commands without `requires` are not affected.\n- Name the fields of errors after their source, as for events ([naming payload fields](#command-events), M022): the\n parameters and the aggregate before the command.\n- `else Error` is required. The error joins the command's errors: it need not be listed in `errors` (listing it\n again is allowed), and several requirements may share one. An unknown error is M007.\n- Requirement names are unique within the command (M002). Without an expression, a requirement needs a label or a\n `///` description (M018), like an invariant.\n- They appear in the Live View (振る舞い tab) and as `@requires` JSDoc on the generated command type (see\n [Generated TypeScript](#generated-typescript)); your command implementation returns the error. The generated\n `check<Command>Requires` (e.g. `checkConfirmOrderRequires`) evaluates them, and the generated tests compare your\n implementation with it.\n- **When several requirements do not hold, the command returns the error of the first one in declaration order.**\n Order the requirements accordingly (the cheapest or most basic first); the generated tests expect exactly that\n error.\n- Not supported: requirements for some source states only (split the command instead), and a generic\n `<Command>Rejected` error.\n- `requires` and `else` are also usable as field and parameter names.\n\n<!-- topic: policies -->\n### Rules that depend on a policy\n\nA policy is a rule the business may change: how many approvals a purchase needs, who may approve, how a credit score\nis computed. There is no policy element; one question decides how to write a rule that depends on one:\n\n**Must the rule keep holding after the policy changes?** An invariant holds at all times, so an invariant over a\npolicy kept outside the aggregate would make existing aggregates invalid as soon as the policy changes. If the rule\nmust hold for this aggregate from now on, fix the policy on the aggregate and write an invariant (1). If the policy\ndecides only whether a command runs, under the policy current at that moment, write a requirement (2). The choice\nstates whether a policy change affects existing data.\n\n**1. Policy fixed on the aggregate**: keep the policy as a value object in the aggregate, taken when the aggregate is\ncreated, and write invariants over it. A later change of the policy applies to new aggregates only.\n\n```dt\n// purchasing/purchase-request.dt\ncontext Purchasing {\n id RequestId: UUID\n primitive Count: Int { value in 0..100 }\n value ApprovalPolicy {\n requiredApprovals: Count\n }\n\n aggregate PurchaseRequest \"購買申請\" {\n id: RequestId\n policy: ApprovalPolicy // the policy when the request was submitted\n approvedBy: PersonId[0..*]\n state Pending\n state Approved {\n invariant enoughApprovals { count(approvedBy) >= policy.requiredApprovals }\n }\n\n create command submit(id: RequestId, policy: ApprovalPolicy) -> Pending\n command approve(person: PersonId)\n Pending -> Pending | Approved\n changes approvedBy\n }\n}\n```\n\n`requiredApprovals` may be an `Int` or, as here, a domain primitive over `Int` or `Decimal`: a domain primitive\ncompares with the numbers `count(…)` and `length(…)` compute.\n\n**2. The current policy decides only whether a command runs**: pass the policy as a parameter of the command and\nstate the condition with `requires`. Existing aggregates stay valid whatever the policy becomes.\n\n```dt\n// purchasing/supplier.dt\ncontext Purchasing {\n id SupplierId: UUID\n id PersonId: UUID\n value ApproverList {\n approvers: PersonId[1..*]\n }\n\n aggregate Supplier \"取引先\" {\n id: SupplierId\n state Candidate\n state Listed\n\n command list(person: PersonId, current: ApproverList)\n Candidate -> Listed\n requires authorized { contains(current.approvers, person) } else NotApprover\n }\n\n error NotApprover\n}\n```\n\n**3. The policy picks a kind of rule**: an enum (or a union, when the kinds carry data) in the aggregate, and one\ninvariant or requirement per kind with `implies`.\n\n```dt\n// purchasing/contract.dt\ncontext Purchasing {\n id ContractId: UUID\n enum SignOff { Single Dual }\n\n aggregate Contract \"契約\" {\n id: ContractId\n signOff: SignOff\n signedBy: PersonId[0..*]\n invariant dualSignOff { signOff == SignOff.Dual implies count(signedBy) >= 2 }\n }\n}\n```\n\n**4. The policy is code** (scores, rule engines, pluggable strategies): declare the invariant or requirement without\nan expression, with a label or `///` description. Your code passes its check to the generated `…With(checks)`\n(`dt guide checks`), which can apply the policy.\n\n```dt\n// purchasing/credit-line.dt\ncontext Purchasing {\n id CreditLineId: UUID\n\n aggregate CreditLine \"与信枠\" {\n id: CreditLineId\n state Open\n\n command extend(amount: Decimal)\n Open -> Open\n requires creditworthy \"与信スコアが基準以上\" else NotCreditworthy // scored by your code\n }\n\n error NotCreditworthy\n}\n```\n\n- Not supported: arithmetic in expressions (`limit * 2`, sums over a collection). Use (4) when a rule needs it.\n- Other aggregates cannot be read from an expression: the policy is a value passed in or kept, never looked up.\n- **A rule across aggregates** is written as one of: the values it needs passed as parameters and checked with\n `requires` (2); the evidence of a check made outside the aggregate, a verified type (`dt guide verified`); a reaction\n to another aggregate's events, a policy (`dt guide reactions`); or a boundary drawn so that the data is in one\n aggregate.\n\n<!-- topic: changes -->\n### Command changes\n\n`changes` lists the fields a command may change, after `requires` and before `emits`. What a command does to the\nother fields follows from its transition:\n\n```dt\n// tickets/ticket.dt\ncontext Tickets {\n id TicketId: UUID\n id PersonId: UUID\n\n aggregate Ticket {\n id: TicketId\n title: String\n note: String?\n state Open {\n assignee: PersonId?\n }\n state InProgress {\n assignee: PersonId\n startedAt: Timestamp\n }\n state Closed {\n closedAt: Timestamp\n }\n\n create command report(id: TicketId, title: String) -> Open // a factory sets every field\n command retitle(title: String, note: String?)\n Open -> Open\n InProgress -> InProgress\n changes title, note // common fields\n command assign(assignee: PersonId?)\n Open -> Open\n changes assignee // carried over from Open to Open, so listed to be changed\n command start(assignee: PersonId, at: Timestamp) Open -> InProgress // no `changes`: sets InProgress's assignee and startedAt, keeps the rest\n command close(at: Timestamp) Open | InProgress -> Closed // sets closedAt; assignee and startedAt are dropped\n }\n}\n```\n\n1. Without `changes`, a command changes **no** common field (strict, like `emits`).\n2. A state-specific field that both the source and the target state have, with the same name and type, is **carried\n over**: it keeps its value, unless it is listed in `changes`. `assignee` is carried over by `assign`\n (`Open -> Open`), not by `start`: it is `PersonId?` in Open and `PersonId` in InProgress.\n3. The target state's other own fields are new, and the command always sets them, without `changes` (`startedAt`,\n and `assignee` in `start`). The source state's own fields that the target does not carry over are dropped.\n4. A `create command` sets every field; `changes` on it is an error.\n5. Without states, a command changes only the fields in `changes`.\n6. A field in `changes` is **given every time**: there is no \"absent = keep\". An optional field is cleared by giving\n `undefined`. A command that changes a field only in some cases gives the old value in the others.\n7. The unit is a top-level field: `changes items` for adding, removing or editing elements of `items`.\n\nThe generated decision of each command enforces these rules (see [Decisions and `decide`](#decisions-and-decide)).\n\n- M021: the names are fields of the aggregate — common fields (also from field sets) or fields of its states — other\n than its identity; not on a `create command`. A state-specific field must be carried over by at least one outcome of\n the command (from a state that has it to one that has it, with the same type): in the other outcomes it is new in\n the target and set anyway, or not in the target and dropped, so listing a field that no outcome carries over would\n change nothing. `retitle` above may list `assignee` (`Open -> Open` carries it over); `start` may not.\n- A field is listed once (M002).\n- `changes` is also usable as a field and parameter name.\n\n<!-- topic: events -->\n### Command events\n\n`emits` is the exact list of the events a command emits **when it succeeds**; on failure (an error) it emits none.\nEach entry is read like a field:\n\n```dt\n// work/work.dt\ncontext Work {\n id WorkId: UUID\n id StepId: UUID\n\n aggregate Work {\n id: WorkId\n state Draft\n state InProgress\n state Completed {\n completedAt: Timestamp\n }\n\n create command open(id: WorkId)\n -> Draft\n emits WorkOpened // the new ID is a parameter\n command plan(steps: StepId[1..*])\n Draft -> Draft\n emits StepPlanned[1..*] // one per step\n command rename(title: String)\n Draft -> Draft\n emits Renamed? // not when the title is the same\n command start() Draft -> InProgress\n command progress(step: StepId, at: Timestamp)\n InProgress -> InProgress | Completed\n emits StepRecorded,\n WorkCompleted when -> Completed // only when it ends in Completed\n }\n\n event WorkOpened {\n workId: WorkId\n }\n event StepPlanned {\n workId: WorkId\n step: StepId\n }\n event Renamed {\n workId: WorkId\n title: String\n }\n event StepRecorded {\n workId: WorkId\n step: StepId\n }\n event WorkCompleted {\n workId: WorkId\n completedAt: Timestamp\n }\n}\n```\n\n- `emits X`: **exactly one** `X`. `X?`: at most one. `X[0..*]`: any number. `X[1..*]`: one or more. `X[n..m]`:\n between `n` and `m` (the generated types check the cases before; other bounds are documented only).\n- `X when Sources -> Targets`: the entry applies only to the outcomes that match — the command starts in one of the\n sources and ends in one of the targets. Either side may be left out (`when Confirmed ->`, `when -> Completed`), not\n both; each side may list states with `|`. A multiplicity comes before `when`: `ItemAdded[1..*] when -> Draft`.\n- The states of `when` must be sources (left) or targets (right) of the command's transitions, and the pattern must\n match at least one of its outcomes (M006). A `create command` has no source: only `when -> X`. Commands of an\n aggregate without states take no `when`.\n- An event is listed once per command (M011): its events are keyed by name in the generated result, so two events of\n different contexts with the same name cannot be emitted by one command.\n- The events of one command have **no order**: they are facts of one decision. With event sourcing, applying the\n events of one command must not depend on their order.\n- **When the contract and the implementation disagree**, follow `@see` on the generated type to the declaration,\n and change the DSL, not the implementation. If following the contract would make the implementation do something\n wrong in business terms — emitting `Renamed` although the title did not change — the contract is what is wrong:\n write `emits Renamed?`. Never emit an event only to satisfy the types, and never leave out one the contract requires.\n- `when` is also usable as a field and parameter name.\n\n**Naming payload fields.** Name the fields of events (and errors) after their source, so that the implementation does\nnot have to guess where a value comes from: a parameter (`step` above), a field of a parameter (`sku` from\n`item: OrderItem`), or a field of the aggregate after the command (`completedAt` of `Completed`). A value computed in\nthe implementation (a total, the days overdue) needs no source. A field that has no source of the same name and type,\nwhile some source has its type, is a warning (M022): `progress(step: StepId, at: Timestamp)` emitting\n`StepRecorded { recordedAt: Timestamp }` would be one — name the parameter `recordedAt`. Exempt are fields of the\naggregate's ID type (M017) and of type Int, Decimal or Bool (counts, sums and flags are mostly computed). Types match\nby base type, optionality aside; a collection only a collection of the same element type. An error's sources are the\nparameters and the aggregate **before** the command (its source states; none for a `create command`). A declarative\nmapping (`emits X { a = b }`) is deferred (see ADR 0021).\n\n<!-- topic: pure -->\n### Pure commands\n\nA command is a **pure function** of the aggregate and its parameters: no I/O, no clock, no randomness, no generated\nIDs. Whatever the decision needs comes in as a parameter — the new aggregate's ID (`open(id: WorkId)`), the time\n(`at: Timestamp`), data of other aggregates. Loading, saving, publishing and calling other services happen around the\ncommand, in your application code. This makes every command reproducible: the same inputs give the same aggregate\nand events, so commands are tested without mocks.\n\nA `create command` without a parameter of the aggregate's ID type is a warning (M020): it would have to make the ID\nitself.\n\nA check that needs more than the aggregate and the parameters — no other customer uses the email address — is made\nbefore the command, and the command takes its evidence as a parameter: a verified type (see\n[Verified types](#verified-types)).\n\n<!-- topic: verified -->\n### Verified types\n\nCommands are pure, so they cannot check what lies outside their aggregate: an email address no other customer uses, an\naddress an outside service confirmed, a budget approved, stock reserved. A verified type is the **evidence that such a\ncheck was made**: a value of a base type that only its factory makes. A command that takes it as a parameter cannot be\ncalled without the check; skipping it is a compile error.\n\n```dt\n// purchasing/purchase-order.dt\ncontext Purchasing {\n id PurchaseOrderId: UUID\n id BudgetId: UUID\n value Spend {\n budget: BudgetId\n amount: Money\n }\n /// 予算の残高で賄えると、発注のときに確かめた支出。\n verified CoveredSpend \"予算内の支出\" of Spend errors OverBudget\n /// 予算内と確かめたうえで、承認者の決裁上限以内と確かめた支出(両方の確認を持つ)。\n verified ApprovedSpend \"承認済みの支出\" of CoveredSpend for PersonId errors OverApprovalLimit, NotApprover\n\n aggregate PurchaseOrder \"発注\" {\n id: PurchaseOrderId\n spend: Spend // stored as the base type\n state Draft\n state Placed\n\n create command open(id: PurchaseOrderId, spend: Spend) -> Draft\n command place(spend: ApprovedSpend) // both checks were made\n Draft -> Placed\n changes spend\n }\n\n error OverBudget\n error OverApprovalLimit\n}\n```\n\n- `verified Name \"label\" of Base for Context? errors E1, E2`. The base is a value type: a primitive type, a domain\n primitive, an ID, an enum, a value object, a union, or another verified type — then the value carries both checks.\n Not an entity or an aggregate (M024).\n- `for` names what the check needs besides the value: the customer to leave out (`for CustomerId?`: absent when a\n customer registers), a tenant, the approver. `?` makes it optional. It is a value type, not a verified one (M024).\n Without `for`, the check needs the value only.\n- `errors` are the errors of the factory, not of the commands that take the value: `changeEmail` above returns no\n `EmailTaken`; the factory did, before the command.\n- A verified type types **command parameters** (and the base of another verified type) only. It is the evidence of a\n check at one moment, so a field of an aggregate, entity, value object, union, field set, event or error stores the\n base type instead (M024, with the hint `store the base type 'Spend'`). A command stores a verified parameter in a\n field of the base type as it is.\n- In invariants and requirements a verified value is a value of its base type (`requires` can compare\n `spend.amount`), and an event or error field of the base type takes its value from a verified parameter of the same\n name (M022).\n- **Use it only for facts that need the outside world**: other aggregates, outside services, the state of the world\n at that moment. A rule over the command's parameters and the aggregate is an invariant or a `requires` (see\n [Command requirements](#command-requirements)); a rule over a policy, see [Rules that depend on a\n policy](#rules-that-depend-on-a-policy).\n- **Evidence of the moment, not a guarantee when the aggregate is saved.** Two customers registering the same address\n at once may both pass the check. Keep enforcing the rule where the data is stored (a unique index), and document it\n on the aggregate as an invariant without an expression (`uniqueEmail` above, in `dt guide aggregates`).\n- `verified`, `of` and `for` are also usable as field and parameter names.\n\nThe generated code (`examples/shop`):\n\n```ts\n/**\n * 未使用のメールアドレス\n * 他の顧客に使われていないことを、登録・変更のときに確かめたメールアドレス。\n * A value of Common.EmailAddress for which MakeAvailableEmail made a check outside the aggregate. Evidence of a check made at that moment, not a guarantee when the aggregate is saved — keep enforcing it where it is stored (e.g. a unique index).\n * Make it with your `MakeAvailableEmail` factory; never `as AvailableEmail` outside it (see dt guide lint).\n * @see sales/customer.dt:5\n */\nexport type AvailableEmail = Common.EmailAddress & { readonly 'verified:Sales.AvailableEmail': true };\n\nexport type MakeAvailableEmail = (value: Common.EmailAddress, context: CustomerId | undefined) => Promise<Result<AvailableEmail, EmailTaken>>;\n```\n\n- The type is the base with a property of its own, one per verified type: a value of the base is not one\n (`Argument of type 'EmailAddress' is not assignable to parameter of type 'AvailableEmail'`), a value with another\n check is not one either, and a value with two checks has both properties. It is assignable to its base, so it is\n stored in a field of the base type without a conversion.\n- The factory `Make<Name>` is async (the check reads repositories and services) and returns a `Result`: the value, or\n one of `errors` (`never` without `errors`, still a `Result`, so every factory is called the same way). Its second\n parameter is the context of `for`: required with `for X`, `X | undefined` with `for X?`, absent without `for`.\n- **You write the factory**, with your repositories and services. It is the only code that may assert the type\n (`email as Sales.AvailableEmail`); a cast anywhere else defeats the purpose — the same weakness as the constructors\n of domain primitives.\n\n```ts\n// examples/shop/src/app/verifications.ts\nexport function verifications(customers: CustomerRepository): Verifications {\n return {\n // `customer` is the one whose email changes, so that keeping one's own address is not \"taken\"; none on register.\n async makeAvailableEmail(email, customer) {\n const holder = await customers.find((c) => c.email === email && c.id !== customer);\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the factory of the verified type: the check above is the evidence\n return holder ? err({ type: 'EmailTaken', email }) : ok(email as Sales.AvailableEmail);\n },\n };\n}\n```\n\nThe generated contract tests do not test the factories (see [Generated tests](#generated-tests)).\n\n<!-- topic: reactions -->\n### Reactions to events (policies)\n\nA policy states a business rule of the form **\"when event X happens, command Y is eventually run\"** — Event\nStorming's policy (the lilac sticky between an event and a command). It is declared in the context that reacts:\n\n```dt\n// billing/policies.dt\ncontext Billing {\n /// 確定した注文は、その合計で請求する。\n policy BillConfirmedOrder \"確定した注文に請求する\" when Sales.OrderConfirmed -> Invoice.issue\n}\n```\n\n- `policy Name \"label\" when Event -> Aggregate.command`. The aggregate is one of the policy's own context, named\n without its context; the event may be of any context (`Sales.OrderConfirmed`). A shared context has no policies.\n- **Only the reaction.** How the command's arguments come from the event, conditions, compensation and ordering are not\n part of the DSL; your code computes the arguments (below).\n- **\"Eventually\", by any mechanism.** The rule holds whether the command runs in the same use case, through an event\n bus, from a queue or by a nightly batch. Nothing in the generated code chooses or forces a mechanism.\n- Declaring a reaction is optional. Declare the ones across contexts at least: they are the dependencies of the\n context map (a policy counts as an **event** dependency of the reacting context on the event's context), and\n `dt show`, the Live View and the language server show them on the command (\"triggered by\") and on the event.\n- M023: the event exists, the aggregate is one of the policy's context (a policy naming an aggregate of another\n context is declared in the wrong context: the hint names the one), the command is one of the aggregate; two\n policies of a context that state the same reaction are a warning. Names are unique in the context (M002).\n- `policy` is also usable as a field and parameter name.\n\nFor each policy, `dt generate ts` writes the type of its **handler**, from the event to what the application needs to\nrun the command, the command's **input**: its parameters by name, after the ID of the aggregate to run it on (named\nlike the aggregate) unless it is a `create command`. An input is written once per command, also when several policies\nrun it.\n\n```ts\n/**\n * What the application needs to run Invoice.issue (a factory): its parameters, by name.\n * Given by the handlers of the policies that run it.\n * @see billing/invoice.dt:14\n */\nexport interface IssueInvoiceInput {\n /** 請求書ID */\n readonly id: InvoiceId;\n /** 注文ID */\n readonly order: Sales.OrderId;\n /** 請求金額 */\n readonly billedAmount: Common.Money;\n}\n\n/**\n * Policy BillConfirmedOrder: when Sales.OrderConfirmed happens, Invoice.issue is run — eventually.\n * …\n */\nexport type BillConfirmedOrderPolicy = (event: Sales.OrderConfirmed) => Promise<IssueInvoiceInput>;\n```\n\nThe handler computes the input: data from elsewhere (the order to bill) and new IDs come in through closures your code\nbuilds, and a handler that cannot compute the input rejects. Your application then runs the command with it, by\nwhatever mechanism.\n\n**Whether handlers are required** is a choice of the configuration (see [Configuration](#configuration)):\n`generate.ts.policies`, overridden per context by `generate.ts.contexts.<Context>.policies`:\n\n- `optional` (default): only the types above; nobody has to use them.\n- `required`: also `<Context>PolicyHandlers`, one handler per policy of the context, and the generated contract tests\n of that context require them (`contract({ …, policies })`; a missing handler is a compile error). They feed each\n handler generated events and check that it resolves to an input the command accepts (see\n [Generated tests](#generated-tests)).\n\n```ts\n// examples/shop/src/app/policies.ts\nexport function billingPolicies({ loadOrder, newInvoiceId }: PolicyDeps): Billing.BillingPolicyHandlers {\n return {\n // a confirmed order is billed for its total\n BillConfirmedOrder: async (event) => {\n const order = await loadOrder(event.orderId);\n if (order?.state !== 'Confirmed') throw new Error(`order ${event.orderId} is ${order ? order.state : 'not found'}, not Confirmed`);\n return { id: newInvoiceId(), order: order.id, billedAmount: orderTotal(order) };\n },\n };\n}\n```\n\n<!-- topic: generated -->\n## Generated TypeScript\n\nPer context a module (`Sales.ts`, …) with ID brands, enums, value objects, entities, unions, a discriminated union per\nstateful aggregate (`DraftOrder | ConfirmedOrder | …`), event / error types with a `type` tag, a function type per\ncommand and an `<Aggregate>Commands` interface to implement, a decision type per command and a shape constant per\naggregate for `decide` (see [Decisions and `decide`](#decisions-and-decide)), plus `index.ts` and `_runtime.ts`. A\ndomain primitive gets a branded type, a constructor contract `Make<Name>` and a generated constructor `make<Name>`;\na verified type its base with a property of its own and the contract of its factory `Make<Name>`, which you write (see\n[Verified types](#verified-types)); invariants and requirements get check functions (see\n[Generated checks](#generated-checks)); a policy the type of its handler and the input of its command (see\n[Reactions to events](#reactions-to-events-policies)). A field set is an interface that the elements using it extend (`interface Expense extends Common.Tenanted`; for a stateful aggregate, its\n`…Common`). The JSDoc of every declaration that comes from the DSL ends with `@see <file>:<line>`, the declaration's\nplace as `dt check` prints it (relative to the config root, e.g. `@see sales/order.dt:51`); editing the `.dt` sources\nmoves these lines, and generating again rewrites them.\n\nThe examples in this section are generated from `examples/shop` in the repository, a fuller version of the Sales\ncontext of the DSL examples (with labels, and `emits NoteChanged?` on `changeNote`).\n\nUnions to name groups of them, each generated only when it has a member:\n\n| Type | Members |\n|---|---|\n| `DomainEvent` | every event of the context |\n| `PersistentEvent` | the events declared `persistent` |\n| `Aggregate` | every aggregate of the context |\n| `<Aggregate>Event` (`OrderEvent`) | the events the aggregate's commands may emit (also of other contexts) |\n\n```ts\n/**\n * Order.confirm (Draft -> Confirmed)\n * 注文を確定する\n * @param at 日時\n * @requires hasItems (明細が1件以上) else EmptyOrder: count(items) > 0\n * @changes none — the command changes no common field\n * @emits OrderConfirmed — exactly once when the command succeeds\n * @see sales/order.dt:51\n */\nexport type ConfirmOrder = (\n order: DraftOrder,\n at: Timestamp,\n) => Result<{\n readonly Confirmed: {\n readonly aggregate: ConfirmedOrder;\n readonly events: { readonly OrderConfirmed: OrderConfirmed };\n };\n}, EmptyOrder>;\n```\n\nThe success value is **keyed by the outcome**: the target state's name (an aggregate without states: the aggregate's\nname, `{ Customer: { aggregate, events } }`), holding the aggregate in that state and its `events`. Every command has\nthis shape, also with one target. With several targets the value is a union, and each branch rules out the other keys,\nso a value holds exactly one outcome:\n\n```ts\nexport type ProgressWork = (\n work: InProgressWork,\n step: StepId,\n at: Timestamp,\n) => Result<\n | {\n readonly InProgress: {\n readonly aggregate: InProgressWork;\n readonly events: { readonly StepRecorded: StepRecorded };\n };\n readonly Completed?: never;\n }\n | {\n readonly Completed: {\n readonly aggregate: CompletedWork;\n readonly events: { readonly StepRecorded: StepRecorded; readonly WorkCompleted: WorkCompleted };\n };\n readonly InProgress?: never;\n },\n never\n>;\n```\n\n`events` is always there, one property per event of that outcome, keyed by the event's name (its `type`; an event of\nanother context by its simple name) and typed by its multiplicity:\n\n| `emits` | property of `events` |\n|---|---|\n| `X` | `X: X` |\n| `X?` | `X?: X` |\n| `X[0..*]` | `X: readonly X[]` (also when empty) |\n| `X[1..*]` | `X: readonly [X, ...X[]]` |\n| `X[n..m]` (others) | `X: readonly X[]`, the bounds in JSDoc (not checked by the types) |\n| none | `NoEvents` (only `{}` is assignable) |\n\nThe JSDoc says the same in words, one `@emits` line per entry (`@emits WorkCompleted — exactly once when it ends in\nCompleted`). Return the value with `ok`, which takes its type from the contract, so that the compiler checks the\nobject exactly: a missing event is reported as `Property 'WorkCompleted' is missing …`, an event the outcome does not\nemit as `… 'WorkCompleted' does not exist in type …`:\n\n```ts\nreturn ok({ Confirmed: { aggregate: confirmed, events: { OrderConfirmed: { type: 'OrderConfirmed', orderId: order.id, confirmedAt: at } } } });\n```\n\nA command takes the aggregate in any source state of its transitions. When every source state has the same outputs\n(`Draft | Confirmed -> Cancelled`), it is a plain function, as above. When the outputs differ by source state — the\ntargets differ by transition, or the events do (`emits RefundRequested when Confirmed ->`) — the command is generic\nover the input state, and its result is the one of that state, looked up in an object keyed by the source states. A\ncaller with a `DraftOrder` gets a `DraftOrder` back, never a `ConfirmedOrder`; a caller with\n`DraftOrder | ConfirmedOrder` gets either:\n\n```ts\nexport type ChangeNoteOrder = <S extends DraftOrder | ConfirmedOrder>(\n order: S,\n note: string | undefined,\n) => {\n /** @emits NoteChanged? — at most once when the command succeeds */\n readonly Draft: Result<{\n readonly Draft: {\n readonly aggregate: DraftOrder;\n readonly events: { readonly NoteChanged?: NoteChanged };\n };\n }, never>;\n /** @emits NoteChanged? — at most once when the command succeeds */\n readonly Confirmed: Result<{\n readonly Confirmed: {\n readonly aggregate: ConfirmedOrder;\n readonly events: { readonly NoteChanged?: NoteChanged };\n };\n }, never>;\n}[S['state']];\n```\n\n<!-- topic: decide -->\n### Decisions and `decide`\n\nA command is implemented as a **decision**: for its outcome, the fields to `set` and the `events`. `decide` from the\nruntime turns a decision into the command, making the aggregate. Every command has a `<Command><Aggregate>Decision`\ntype with **one handler per source state**, keyed by the state's name — also with a single source state, so that every\ncommand is written the same way. A `create command` and a command of an aggregate without states have one function\ninstead.\n\n```ts\n/**\n * Order.confirm, decided per source state; implement it with `decide(OrderShape, …)` from the runtime.\n * Each outcome gives the fields of `set`, and `decide` makes the aggregate, taking the other fields from the input.\n * @requires hasItems (明細が1件以上) else EmptyOrder: count(items) > 0\n * @changes none — the command changes no common field\n * Draft -> Confirmed — set: confirmedAt (new in Confirmed); carried over: id, items, customer, note\n * @see sales/order.dt:51\n */\nexport type ConfirmOrderDecision = {\n /** @emits OrderConfirmed — exactly once when the command succeeds */\n readonly Draft: (order: DraftOrder, at: Timestamp) => Result<{\n readonly Confirmed: {\n readonly set: { readonly confirmedAt: Timestamp };\n readonly events: { readonly OrderConfirmed: OrderConfirmed };\n };\n }, EmptyOrder>;\n};\n```\n\n`set` holds exactly what the command decides, by the rules of [Command changes](#command-changes): the target state's\nnew fields and the fields in `changes`. Every one is required; an optional field is typed `T | undefined`, and an\noutcome with nothing to set has `NoChanges` (only `{}` is assignable). Anything else is a compile error that names the\nfield: `items` in the `set` of `confirm` is reported as\n`'items' does not exist in type '{ readonly confirmedAt: Timestamp; }'`, a missing `confirmedAt` as\n`Property 'confirmedAt' is missing in type '{}' …`. The JSDoc lists `@changes` and, per outcome, what is set, carried\nover and dropped.\n\n`decide(shape, decision)` calls the handler of the aggregate's `state` with the command's arguments. For the outcome\nit returns, it makes the aggregate from the fields of `set`, the other fields of the target state taken from the input\n(the common fields and the carried-over state-specific fields) and the target `state`; the input's other fields are\ndropped. A factory's aggregate is its `state` and `set`. Events and errors pass through unchanged. The shape is the\ndata `decide` needs, a constant generated per aggregate:\n\n```ts\n/**\n * The fields of Order as data for `decide`: the common fields, and the fields of each state on top of them.\n * @see sales/order.dt:13\n */\nexport const OrderShape: Shape = {\n fields: ['id', 'items', 'customer', 'note'],\n states: { Draft: [], Confirmed: ['confirmedAt'], Cancelled: ['reason'] },\n};\n```\n\nPass the decision type as the type argument, so that the handlers are typed from it. The result is assignable to the\ncommand's contract without a type assertion, and each handler is checked against its own transition — deciding the\nConfirmed outcome in the `Draft` handler of `changeNote`, or leaving a handler out, is a compile error:\n\n```ts\n// src/note.ts — your code\nimport { Sales, decide, err, ok } from '../generated/domain/index.js';\n\nconst noteChanged = (order: Sales.Order, note: string | undefined) =>\n note === order.note ? {} : { NoteChanged: { type: 'NoteChanged', orderId: order.id, note } as const };\n\nexport const changeNote: Sales.OrderCommands['changeNote'] = decide<Sales.ChangeNoteOrderDecision>(Sales.OrderShape, {\n Draft: (order, note) => ok({ Draft: { set: { note }, events: noteChanged(order, note) } }),\n Confirmed: (order, note) => ok({ Confirmed: { set: { note }, events: noteChanged(order, note) } }),\n});\n\nexport const confirm: Sales.OrderCommands['confirm'] = decide<Sales.ConfirmOrderDecision>(Sales.OrderShape, {\n Draft: (order, at) => {\n if (order.items.length === 0) return err({ type: 'EmptyOrder' });\n return ok({ Confirmed: { set: { confirmedAt: at }, events: { OrderConfirmed: { type: 'OrderConfirmed', orderId: order.id, confirmedAt: at } } } });\n },\n});\n```\n\nThe contract can still be implemented directly, returning the aggregate itself; then nothing keeps the command from\nchanging a field it does not list, and a generic contract needs a type assertion on its result (TypeScript does not\nnarrow `S` inside the implementation).\n\nCode that handles any command — saving the aggregate, publishing the events — uses `outcome` and `eventList` from the\nruntime. `outcome(value)` gives `{ to, aggregate, events }` (`to` is the key: the target state). `eventList(events)`\ngives the events as one list: arrays flattened, absent optional events skipped, sorted by event type (an order of\nconvenience: the contract has none).\n\n```ts\n// src/confirm.ts — your code: the application around a command\nimport { eventList, outcome, type Sales } from '../generated/domain/index.js';\n\nexport interface OrderStore {\n save(order: Sales.Order, events: readonly Sales.OrderEvent[]): Promise<void>;\n}\n\nexport async function confirm(commands: Sales.OrderCommands, store: OrderStore, order: Sales.DraftOrder, at: Date): Promise<Sales.EmptyOrder | undefined> {\n const r = commands.confirm(order, at); // the time is a parameter: the command is pure\n if (!r.ok) return r.error;\n const { aggregate, events } = outcome(r.value);\n await store.save(aggregate, eventList(events));\n return undefined;\n}\n```\n\nA command's requirements are documented on its type and on its decision, one `@requires` line each, and the\nerrors of their `else` are part of its error type (after those of `errors`, each once):\n\n```ts\n/**\n * Review.accept (Reviewing | Escalated -> Accepted)\n * 受入\n * @requires authorizedApprover (承認者である) else NotApprover: contains(policy.approvers, person)\n * @requires noOpenBlocker (未解決の阻害がない) else Blocked: openBlockers == 0\n * @requires signedOff (本人の承認が記録済み) else NotSignedOff: not expressed in the DSL\n * @changes none — the command changes no common field\n * @emits nothing\n * @see review/review.dt:24\n */\nexport type AcceptReview = (\n review: ReviewingReview | EscalatedReview,\n person: PersonId,\n) => Result<{\n readonly Accepted: {\n readonly aggregate: AcceptedReview;\n readonly events: NoEvents;\n };\n}, Conflict | NotApprover | Blocked | NotSignedOff>;\n```\n\n<!-- topic: your-code -->\n### Generated code and your code\n\nThe generated files are the **contract**, your code is the **implementation**:\n\n- Generated: types — the shapes of the model, the command signatures (`ConfirmOrder`, `OrderCommands`) and their\n decisions (`ConfirmOrderDecision`), and the constructor signatures of domain primitives (`MakeQuantity`) — plus\n data without logic: one shape constant per aggregate (`OrderShape`), the field names `decide` needs — plus logic\n derived 1:1 from DSL expressions: the checks of invariants and requirements, the equality of value types and the\n constructors of domain primitives (see [Generated checks](#generated-checks)). Invariants and requirements also\n appear as JSDoc (`@invariant`, `@requires`). `_runtime.ts` holds a few helpers (`ok`, `err`, `decide`, `outcome`,\n `eventList`, `parseDecimal`, and those of the checks) that are the same for every model.\n- Yours: the behaviour — command implementations (returning the error of the first requirement that does not hold;\n pure functions, see [Pure commands](#pure-commands)), the factories of verified types (the checks outside the\n aggregate, see [Verified types](#verified-types)) and the constraints the DSL does not express, which you pass to\n the generated checks — in your own files, which import the generated modules (never the other way round) and are\n type-checked against them. Your code may call the generated checks, or implement its own (a hand-written\n `Make<Name>` like the one below); the generated tests use the generated checks as the oracle either way.\n\n```ts\n// src/primitives.ts — your code\nimport { err, ok, type Sales } from '../generated/domain/index.js';\n\nexport const makeQuantity: Sales.MakeQuantity = (value) =>\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the factory: the check is on this line\n Number.isInteger(value) && value >= 1 && value <= 999 ? ok(value as Sales.Quantity) : err({ type: 'InvalidQuantity', value, invariant: 'valid' });\n```\n\nDo not edit generated files: change the `.dt` sources and generate again. Adding properties or methods to generated\ntypes (declaration merging, editing the output) is not supported; write functions that take the generated types.\n\nA complete example is [`examples/shop`](https://github.com/hisamekms/domaintrail/tree/main/examples/shop) in the\nrepository: the model, what `dt generate ts` writes from it, and the code written against it — the command\nimplementations and a small application layer around them (repositories, an event store, policies across contexts),\nwith the contract tests and a demo.\n\n<!-- topic: lint -->\n### Linting your code\n\nA domain type is a brand: `Sales.OrderId` is a `string` the compiler tells apart, `Common.Amount` a `Decimal` that holds\nits invariants, `Sales.AvailableEmail` the evidence of a check. A cast (`'x' as Sales.OrderId`, `amount as\nCommon.Amount`) makes one without its constructor, so nothing checks the invariants or makes the check: never cast to\na domain type, a verified type or `Decimal`. Make them instead:\n\n| Type | Make it with |\n|---|---|\n| ID, domain primitive | `make<Name>` (generated; or your own `Make<Name>`), e.g. `Sales.makeOrderId('order-1')` |\n| verified type | its factory `Make<Name>`, which you write (see [Verified types](#verified-types)) |\n| `Decimal` | `parseDecimal(text)` from the runtime: digits with an optional `-` and fraction, else `undefined` |\n\nA value that cannot fail to be valid (a sum of non-negative amounts is an `Amount`) still goes through its constructor;\na failure there is a bug, so throw. In tests, make the values the same way and assert the result rather than cast a\nfallback. The generated JSDoc of every branded type says how to make it (`Make it with makeAmount; never as Amount`).\n\nLet the linter enforce it with the type-aware rule that reports narrowing casts (it allows `as const` and widening):\noxlint's `typescript/no-unsafe-type-assertion` (with `--type-aware`, which needs `oxlint-tsgolint`) or typescript-eslint's\n`@typescript-eslint/no-unsafe-type-assertion`. Leave the generated directories out: they make the domain types, so they\ncast.\n\n```jsonc\n// .oxlintrc.json — run `oxlint --type-aware`\n{\n \"plugins\": [\"typescript\"],\n \"rules\": { \"typescript/no-unsafe-type-assertion\": \"error\" },\n \"ignorePatterns\": [\"generated/**\"]\n}\n```\n\n```js\n// eslint.config.js — typescript-eslint with type information\nimport tseslint from 'typescript-eslint';\n\nexport default tseslint.config(\n { ignores: ['generated/**'] },\n {\n files: ['**/*.ts'],\n extends: [tseslint.configs.base],\n languageOptions: { parserOptions: { projectService: true } },\n rules: { '@typescript-eslint/no-unsafe-type-assertion': 'error' },\n },\n);\n```\n\nThe legitimate casts are in factories only — your `Make<Name>`, a verified type's factory — each with a disable\ncomment that says why: `// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the factory: …` (ESLint:\n`// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- …`).\n\n`dt generate ts` prints `hint: no lint rule against casts to domain types found (no-unsafe-type-assertion); see dt guide\nlint` (in `--format json`, in `hints`) when no lint config mentions the rule: it looks for `.oxlintrc.json`,\n`.oxlintrc.jsonc`, `oxlint.config.*` and `eslint.config.*` in the config's directory and the directories above it up to\nthe top of the git repository (only the config's directory without one). It is a heuristic — the files are searched as\ntext for `no-unsafe-type-assertion`, not evaluated — so a rule shared from a package is not found: set\n`\"lintHint\": false` in `domaintrail.jsonc` then.\n\n<!-- topic: checks -->\n### Generated checks\n\nEvery invariant and requirement with an expression becomes a pure function in the context's module, translated 1:1\nwith the semantics of [Invariant expressions](#invariant-expressions): equality by value / all fields / `id` / tag and\nfields, `exists` and `implies` as written, regular expressions as JavaScript (`matches` is `RegExp.test`), `length` as\nthe string's `length`, Decimals (carried as strings) and Ints compared as numbers, Timestamps by time.\n\n| Generated | For | Returns |\n|---|---|---|\n| `check<Name>(value)` | a domain primitive with invariants | the violated invariants, in declaration order (empty: valid) |\n| `make<Name>` | every domain primitive and ID | implements `Make<Name>`: `Invalid<Name>` with the **first** violated invariant |\n| `check<Name>(x)` | a value object, entity or union with invariants (a union: those of its variant) | the violated invariants |\n| `check<State><Aggregate>(x)`, `check<Aggregate>(x)` | an aggregate with invariants | per state, the aggregate's and the state's violated invariants; `check<Aggregate>` for any state |\n| `check<Command>Requires(aggregate, …params)` | a command with requirements | the name of the first requirement that does not hold, or `undefined` |\n| `equal<Name>(a, b)` | every value object, entity and union | the equality of `==`, `unique` and `contains` |\n\n```ts\n/** The first requirement of Order.confirm that does not hold, in declaration order; `undefined` when all hold. */\nexport function checkConfirmOrderRequires(order: DraftOrder, _at: Timestamp): 'hasItems' | undefined {\n if (!(order.items.length > 0)) return 'hasItems';\n return undefined;\n}\n```\n\nA constraint without an expression cannot be generated. Instead the check gets a **slot**: a type naming the\nfunction your code provides, and a `…With` variant that takes it and combines it with the generated parts, in\ndeclaration order. Generated code never imports your code; you pass the function in:\n\n```ts\n// generated (Sales.ts), for `invariant reserved \"予約語でない\"` in `primitive Sku`\nexport type SkuDeclaredChecks = { readonly reserved: (value: string) => boolean };\nexport function checkSkuWith(checks: SkuDeclaredChecks): (value: string) => readonly InvalidSku['invariant'][];\nexport const makeSkuWith: (checks: SkuDeclaredChecks) => MakeSku;\n\n// your code\nexport const makeSku = Sales.makeSkuWith({ reserved: (value) => !RESERVED.has(value) });\n```\n\nThe same holds for the other checks: `<Type>DeclaredChecks` and `check<Type>With(checks)` for value types and\naggregates (one object per aggregate, keyed by invariant name, for all its states), and\n`<Command>DeclaredRequires` and `check<Command>RequiresWith(checks)` for requirements. A type without such a\nconstraint has only the plain function.\n\n<!-- topic: tests -->\n### Generated tests\n\nWith `generate.ts.tests` in the config, `dt generate ts` also writes **contract tests** into `<out>/tests` (or\n`generate.ts.tests.out`). They check what the types cannot — the values — and use the generated checks as the oracle:\n\n| | Checks |\n|---|---|\n| T1 | `make<Name>` accepts exactly the values that hold the invariants, with values at and next to each bound of ranges and lengths and strings for regular expressions, and reports the first one violated |\n| T2 | when a requirement does not hold, the command returns the `else` error of the first such requirement; when all hold, none of the `else` errors |\n| T3 | after a successful command, the aggregate holds the invariants of its state (the aggregate's and the state's) |\n| T4 | events whose bounds the types do not express (`X[2..5]`) are emitted that many times |\n| T5 | purity: the same input twice gives deep-equal results, and the input is not changed |\n| T6 | with `required` policies: for every event, the handler of a policy resolves to an input the command accepts — every argument present unless optional, of its type (the fields of value objects and entities in turn), holding the invariants of its type, a list within its multiplicity |\n\nInputs are generated with [fast-check](https://fast-check.dev) from the types: IDs, ranges, lengths and regular\nexpressions of domain primitives, enums, value objects, entities, unions, collections with their multiplicities, and\naggregates per state. A parameter of a verified type gets values of its base type, taken as verified (the check\npassed); `arbitraries` overrides it like any other type (`{ Sales: { AvailableEmail: … } }`). **Inputs are always valid**: only values that hold their invariants are used, and parameters\nare values of their types; what a command does with invalid input is outside its contract. Values of one type are\ndrawn often from a small pool, so that duplicates, `contains` and `!=` are decided both ways. Runs are deterministic\n(fixed seed, configurable), and a failure shows the shrunk counterexample and the seed. Install fast-check as a dev\ndependency of your project; generated product code does not use it.\n\nPer context, `tests/<Context>.ts` exports `contract(impl, options?)`, returning a suite of named test cases\n(`{ name, children }` of `{ name, run, todo? }`) for any runner, and `arbitraries(options?)`. `impl` lists what to\ntest, each optional: the constructors (`make: { Quantity: makeQuantity }`) and the commands per aggregate\n(`Order: orderCommands`); for a context whose policies are `required`, also `policies` (its `<Context>PolicyHandlers`),\nwhich is not optional. The handlers run as your code wires them, with whatever data they read: give them what the\ngenerated events need (below, a confirmed order for any ID). With `generate.ts.tests.runner` (`bun`, `vitest`, `jest`, `node`), `tests/_register.ts`\nregisters a suite with that runner, and the test file is a few lines:\n\n```ts\n// examples/shop/src/contract.test.ts\nimport { parseDecimal, type Sales as SalesTypes } from '../generated/domain/index.js';\nimport { Billing, Common, Sales, register } from '../generated/domain/tests/index.js';\nimport { billingPolicies } from './app/policies.js';\nimport { customerCommands } from './domain/customer-commands.js';\nimport { invoiceCommands } from './domain/invoice-commands.js';\nimport { orderCommands } from './domain/order-commands.js';\nimport { makeAmount, makeCustomerId, makeEmailAddress, makeInvoiceId, makeOrderId, makeOrderItemId, makeQuantity, makeSku } from './domain/primitives.js';\n\nregister(\n Sales.contract({\n make: { Quantity: makeQuantity, Sku: makeSku, CustomerId: makeCustomerId, OrderId: makeOrderId, OrderItemId: makeOrderItemId },\n Order: orderCommands,\n Customer: customerCommands,\n }),\n);\nconst ok = <T>(r: { ok: true; value: T } | { ok: false }): T => {\n if (!r.ok) throw new Error('invalid example');\n return r.value;\n};\n\n// The policy cases feed Billing's handlers generated `OrderConfirmed` events, so the order a handler reads comes from\n// here: a confirmed order with that ID. The tests run the handlers with whatever they are wired to.\nconst confirmed = (id: SalesTypes.OrderId): SalesTypes.ConfirmedOrder => ({\n state: 'Confirmed',\n id,\n customer: makeCustomerId('customer-1'),\n items: [\n {\n id: makeOrderItemId('item-1'),\n sku: ok(makeSku('ABC-0001')),\n quantity: ok(makeQuantity(3)),\n unitPrice: { amount: ok(makeAmount(parseDecimal('12.50')!)), currency: 'USD' },\n },\n ],\n confirmedAt: new Date('2026-10-24T09:00:00Z'),\n});\nlet invoices = 0;\nconst policies = billingPolicies({ loadOrder: async (id) => confirmed(id), newInvoiceId: () => makeInvoiceId(`invoice-${++invoices}`) });\n\nregister(Billing.contract({ make: { InvoiceId: makeInvoiceId }, Invoice: invoiceCommands, policies }));\nregister(Common.contract({ make: { EmailAddress: makeEmailAddress, Amount: makeAmount } }));\n```\n\nOptions, all optional:\n\n- `seed`, `runs`: fast-check's seed (default 42) and the number of generated inputs per case (default 100).\n- `arbitraries: { Sales: { Sku: fc.stringMatching(/^ABC-\\d{4}$/) } }`: replaces the arbitrary of a type, by context\n and type name (aggregates per state, e.g. `DraftOrder`). Its values are still filtered by the invariants. When\n filtering rejects 1000 values in a row, the case fails with a message naming the type to override.\n- `declared: { Sales: { Sku: { reserved: … }, Order: { … }, AcceptReview: { signedOff: … } } }`: the checks of the\n constraints without an expression, by context and type or command type; the same functions your code passes to the\n `…With` checks. A constraint whose check is not passed is listed as a **todo** case (\"not expressed in the DSL and\n not checked\"), and the tests accept either outcome where it would decide.\n- `examples: { Order: { states: { Draft: [order] }, commands: { confirm: [[order, at]] } } }`: valid aggregates per\n state (mixed into the generated ones) and arguments per command (run first, before the generated ones). Examples\n must hold their invariants; one that does not fails the case.\n\nA failing case says what was expected, e.g. `Order.cancel(…) made an aggregate that violates 'otherNeedsNote'`. Change\nthe implementation — or, if the implementation is right, the DSL (here a missing `requires` on `cancel`).\n\n**Outside the generated tests** is what happens around the commands, which your own tests cover (as\n`examples/shop/src/app.test.ts` does): the factories of verified types (`MakeAvailableEmail`, with your repositories and\nservices), the constraints without an expression where you pass no check, and the application — loading, saving,\npublishing events.\n\n<!-- topic: metadata -->\n### Technical metadata\n\nTechnical fields (see [Domain fields and technical fields](#domain-fields-and-technical-fields)) are added by your code,\naround the generated types. Keep the two apart in an envelope, `{ event, meta }`, rather than an intersection\n(`E & Meta`): names cannot clash, and domain and technical fields stay separate. `dt` generates no envelope type.\n\n```ts\n// src/stored.ts — your code: metadata of stored events and aggregates\nimport type { Sales } from '../generated/domain/index.js';\n\nexport interface EventMeta {\n readonly eventId: string;\n readonly recordedAt: Date;\n readonly correlationId?: string;\n readonly causationId?: string;\n}\n\n/** What Sales records in addition. */\nexport interface SalesEventMeta extends EventMeta {\n readonly accountId: string;\n}\n\nexport type Stored<E, M extends EventMeta = EventMeta> = { readonly event: E; readonly meta: M };\n\nexport type StoredSalesEvent = Stored<Sales.PersistentEvent, SalesEventMeta>;\n\nexport interface AggregateMeta {\n readonly version: number;\n readonly createdAt: Date;\n readonly updatedAt: Date;\n}\n\nexport type Versioned<A, M extends AggregateMeta = AggregateMeta> = { readonly aggregate: A; readonly meta: M };\n\nexport interface OrderRepository {\n load(id: Sales.OrderId): Promise<Versioned<Sales.Order> | undefined>;\n save(order: Versioned<Sales.Order>, events: readonly Sales.OrderEvent[]): Promise<void>;\n}\n```\n\nMetadata that only some kinds of event carry can be keyed by the event's `type`:\n\n```ts\n// src/meta-by-event.ts — your code\nimport type { Sales } from '../generated/domain/index.js';\nimport type { EventMeta } from './stored.js';\n\ninterface MetaByEvent {\n OrderPlaced: { readonly channel: 'web' | 'store' };\n}\n\nexport type MetaOf<E extends { type: string }> = EventMeta & (E['type'] extends keyof MetaByEvent ? MetaByEvent[E['type']] : {});\n\n/** One envelope per kind of event, so that a union of events gives a union of envelopes. */\nexport type StoredEvent<E extends { type: string }> = E extends unknown ? { readonly event: E; readonly meta: MetaOf<E> } : never;\n\nexport const channelOf = (s: StoredEvent<Sales.OrderPlaced>): 'web' | 'store' => s.meta.channel;\nexport type StoredSalesEvents = StoredEvent<Sales.DomainEvent>;\n```\n\n<!-- topic: output-dirs -->\n### Output directories\n\nBy default every context goes to `generate.ts.out`. Contexts listed in `generate.ts.contexts` go to their own\ndirectory instead, e.g. a shared context into a package used by several applications. Each directory written to gets\nits own `_runtime.ts` and an `index.ts` re-exporting only the contexts in it; a module imports other contexts by\nrelative path (`import type * as Common from '../../packages/shared/src/generated/Common.js'`). The runtimes are\nidentical, so values typed with one are accepted where the other is expected.\n\nEach run replaces the files generated before (recognized by their header). The files written are recorded in\n`.domaintrail/generated.json` under the root, and generated files of earlier runs that are no longer written (an output\ndirectory moved, a context removed) are deleted, along with a directory left empty. Only recorded paths inside the root\nand not reached through a symlink are deleted; other entries in that file are ignored. Other files are never deleted —\nneither hand-written files nor generated files you edited — and generation stops instead of overwriting one.\n<!-- topic: rules -->\n## Rules\n\nEach diagnostic names its rule. A `hint` says what to change when the message does not; unresolved names get\n`did you mean …?` instead.\n\n| Code | Check |\n|---|---|\n| SYNTAX | lexer / parser errors |\n| M001 | types, aggregates and names in invariants and requirements resolve |\n| M002 | names are unique in their scope (also across files of one context) |\n| M003 | an entity is owned by exactly one aggregate (warning when owned by none) |\n| M004 | no ownership across aggregate boundaries (entities need `owns`, aggregates need `references`, …) |\n| M005 | a reference's ID type matches the identity of the target aggregate |\n| M006 | command transitions: states exist; stateful: `create` has one `-> X`, others one or more `A -> B` with sources; a state is the source of one transition per command; stateless: none. `emits … when`: its states are sources / targets of the command, it matches an outcome, a factory selects by target only, stateless: no `when` |\n| M007 | `emits` / `errors` / `else` refer to existing events / errors |\n| M008 | an invariant or requirement is a Bool expression |\n| M009 | operators and operands have compatible types; a domain primitive compares with the same one, literals, and (over `Int` / `Decimal`) `count(…)` / `length(…)` |\n| M010 | state-specific fields are used only in their own state's invariants, and in requirements of commands whose every source state has them |\n| M011 | generated TypeScript names do not collide, including the events of one command (keyed by name) |\n| M012 | the ownership graph has no cycles |\n| M013 | aggregates and entities have an `id` field of their own ID type |\n| M014 | shared contexts: only value types, no dependency on other contexts, consistent `shared`, unambiguous names (shadowing is a warning) |\n| M015 | unions: at least one variant, variants match the bound enum exactly, no variant field named like the discriminator |\n| M016 | field sets: plain fields only (no `id`), `with` only on aggregates, entities and events, each field name from one place |\n| M017 | (warning) an event emitted by an aggregate's command has a field of that aggregate's ID type |\n| M018 | an invariant or requirement without an expression has a label or a `///` description |\n| M019 | a parameter of a command with `requires` is not named like a field its requirements see |\n| M020 | (warning) a `create command` has a parameter of the aggregate's ID type (commands are pure: the ID is passed in) |\n| M021 | `changes` names fields the command can change: fields of the aggregate other than its identity, a state-specific one carried over by an outcome; none on a `create command` |\n| M022 | (warning) a field of an event or error a command emits or returns, with no source of the same name and type (parameters, their fields, the aggregate's fields), while some source has its type; ID-typed, Int, Decimal and Bool fields are exempt |\n| M023 | policies: the event exists, the aggregate is one of the policy's own context, the command is one of the aggregate, the event is not of a shared context; (warning) two policies of a context state the same reaction |\n| M024 | verified types: the base and the context of `for` are value types (`for` not a verified type), no cycle; a verified type types command parameters and other verified types only, no field |\n| L001 | (warning) an inline field label disagrees with the term, or context fragments disagree on the context name |\n| L002 | (warning) a term is not used by any field or parameter |\n| L003 | (warning) different things in one scope share a Japanese name |\n\n<!-- topic: show -->\n## Looking up one element\n\nTo understand one element of a large model, run `dt show <name>` instead of reading the `.dt` files: it prints the\nelement resolved, close to the DSL — the fields of field sets inline (`// with Common.Tenanted`), each field's label\nwith where it comes from (`(from term)`, `(from type)`; none when written inline), multiplicities, `owns` /\n`references -> X`, invariants with their expressions (or `(not expressed in the DSL)`), states with their own fields\nand invariants, commands with their transitions, `requires … else`, `changes`, the events of each outcome (`when`\nresolved), the full error union, a verified type's `of`, `for` and `errors`, a `// file:line` location per declaration\n(relative to the root), and who uses it (owns, references, its type or ID, emits, returns, `with`, verified types).\n\n```bash\ndt show # the contexts and their elements\ndt show Sales # a context: terms and one line per element\ndt show Sales.Order # an element (Order alone is enough when no other context has one)\ndt show Sales.Order.confirm # a command: its contract, the fields each outcome sets / carries / drops, payloads\ndt show Sales.Order.Confirmed # a state: its fields, invariants, the commands that enter and leave it\n```\n\nA name may leave out leading parts (`Order`, `Order.confirm`) when it names one thing only; when it names several, they\nare listed (exit 1); an unknown name gets `did you mean …?` (exit 1). It works on a model with semantic errors as far\nas it resolves (unresolved types are marked `(unresolved)`, and a note on stderr gives the number of errors): the exit\ncode is 0 whenever the name was found, whatever the diagnostics. A model with syntax errors cannot be shown (exit 1).\n\n`--format json` prints one JSON document, the Typed Model of what was found, keyed by `show`:\n\n- `{\"show\": \"element\", \"element\": {...}, \"usedBy\": {...}}`: the element (`id`, `kind`, `fields` with `label`,\n `labelSource`, `multiplicity`, `fieldSet`, …, `invariants`, `states`, `commands`, `location`); each command has\n `outcomes`: `[{\"from\", \"to\", \"emits\": [{\"event\", \"multiplicity\", \"lower\", \"upper\"}], \"sets\", \"carries\", \"drops\"}]`.\n A verified type has `verified`: `{\"of\", \"for\": {\"type\", \"optional\"}, \"errors\"}`; a policy has `policy`:\n `{\"event\", \"aggregate\", \"command\"}`. `usedBy` has the lists `takenOnBy`, `ownedBy`, `typeOf`, `referencedBy`,\n `idUsedBy`, `emittedBy`, `returnedBy` (commands, and verified types whose factory returns the error), `verifiedAs`\n (verified types of it), `contextOf` (verified types whose `for` it is), `discriminatorOf` and `reactedToBy` (the\n policies that react to the event), as dotted paths (`Billing.Invoice.order`, `Sales.Order.addItem(item)`). Each\n command has `triggeredBy`: the policies that run it.\n- `{\"show\": \"command\", \"aggregate\", \"command\"}` (with `outcomes` and `triggeredBy`), `{\"show\": \"state\", \"aggregate\", \"state\", \"enteredBy\",\n \"leftBy\"}`, `{\"show\": \"context\", \"context\"}` and `{\"show\": \"overview\", \"contexts\"}` (name, label, doc, shared, terms\n and the elements with their location).\n- `\"modelErrors\": n` when the model has errors; `{\"error\": \"...\", \"candidates\": [...]}` when the name is unknown\n (the suggestions) or ambiguous (the matches).\n\n<!-- topic: fmt -->\n## Formatting\n\n`dt fmt` rewrites every file of the workspace (the config's `sources`, like `check`) in the one canonical layout;\n`dt fmt <files…>` formats only those files (paths relative to the current directory). Run it after every edit, so that\nthe same model is always written the same way and a diff shows only what changed. `dt fmt --check` writes nothing,\nlists the files that are not formatted and exits 1 if there is one (for CI). A file with syntax errors is reported\nlike `dt check` does and left untouched (exit 1). The language server formats a document the same way.\n\nFormatting changes only whitespace and three spellings: `[0..1]` becomes `?`, `[1..1]` is removed, and the commas\nbetween enum values or between transitions are dropped. The model never changes, strings (regular expressions\nincluded) are untouched, and every comment stays in its place: on the line of the code before it, or on a line of its\nown. The layout:\n\n- 2 spaces of indentation per level. Every member of a block (`context`, `aggregate`, `state`, `value`, `event`, `terms`,\n …) is on its own line; a block without members is `{}`. One blank line between members is kept where the author wrote\n one or more, none after `{` or before `}`. Contexts in one file are separated by one blank line.\n- An enum stays on one line (`enum Currency { JPY \"日本円\" USD \"米ドル\" }`), unless it has comments or would be longer\n than 120 columns (a Japanese character counts as two); then one value per line.\n- An expression body stays inline (`invariant positive { amount > 0 }`), unless it has comments or its line would be\n longer than 120 columns; then the expression goes on a line of its own between `{` and `}`. An expression is never\n broken.\n- A command with at most one clause — a transition, `requires`, `changes`, `emits` with one event, or `errors` — and\n no comments stays on one line if it fits in 120 columns: `command start() Draft -> InProgress`. Otherwise each clause\n goes on a line of its own, one level deeper, in the order of the grammar, and a list of several events after `emits`\n takes one line per event, aligned after `emits `:\n\n ```text\n command cancel(reason: CancelReason)\n Draft | Confirmed -> Cancelled\n emits OrderCancelled,\n RefundRequested when Confirmed ->\n ```\n\n- Parameters, `with`, `changes` and `errors` lists stay on one line. One space after `:` and `,` and around `->`, `|`,\n `=>` and operators; none inside `( )`, `[ ]`, around `.` and `..`, or before `?`. One space before a comment at the\n end of a line.\n\n<!-- topic: output -->\n## Output and exit codes\n\n`check` (and `generate ts`, which checks first) prints one line per problem, sorted by file and position, then a\nsummary. When the message does not say how to fix the problem, an indented `hint:` line follows: what to change, or\n`did you mean …?` with the closest names of the right kind visible there (at most 3) for a name that does not resolve.\n\n```text\nsales/order.dt:2:11 - warning L002: Term 'notes' (\"備考\") is not used by any field or parameter in context 'Sales'.\n hint: did you mean 'note'?\nsales/order.dt:8:32 - error M009: count() requires a collection, but got 'Money'.\nsales/order.dt:12:30 - error M006: Cannot resolve state 'Confirmd' in this aggregate.\n hint: did you mean 'Confirmed'?\n1 file(s) checked: 2 error(s), 1 warning(s).\n```\n\nPaths are relative to the root, lines and columns 1-based. Errors (and their hints) go to stderr, warnings to stdout.\nFix errors first; warnings do not fail the check. Other failures (missing root, invalid config, port in use, …) are one\nline on stderr.\n\nWith `--format json` (`check` and `generate ts`), the output is one JSON document on stdout instead, with the same\norder and exit codes (JSON Schema: `schema/diagnostics.schema.json` in the package):\n\n```json\n{\n \"diagnostics\": [\n { \"file\": \"sales/order.dt\", \"line\": 12, \"column\": 30, \"endLine\": 12, \"endColumn\": 38, \"severity\": \"error\",\n \"code\": \"M006\", \"message\": \"Cannot resolve state 'Confirmd' in this aggregate.\", \"hint\": \"did you mean 'Confirmed'?\" }\n ],\n \"summary\": { \"files\": 6, \"errors\": 1, \"warnings\": 0 }\n}\n```\n\n`endLine` / `endColumn` point just past the problem; `hint` is present only when there is one. `generate ts` adds\n`written` (the output directories). A failure other than the diagnostics (missing root, invalid config, no `.dt` file,\ngeneration refused) is an `error` message in the document, e.g. `{ \"error\": \"Root directory not found: domain\" }`;\n`diagnostics` and `summary` are there whenever the check ran.\n\n| Exit code | Meaning |\n|---|---|\n| 0 | success (`check`: no errors; warnings are allowed) |\n| 1 | the model has errors, no `.dt` file was found, generation was refused, `show` did not find the name (or it is ambiguous), `guide` got an unknown topic, or the root / a file could not be read |\n| 2 | invalid configuration (`domaintrail.jsonc` / `--config`, including a context name in `generate.ts.contexts` that the model does not have) or an unknown `generate` target |\n\n<!-- topic: config -->\n## Configuration\n\n`domaintrail.jsonc` (comments and trailing commas allowed) or `domaintrail.json` is looked up in `--root` (`-r`,\ndefault `.`) and its parents, like `tsconfig.json`; `--config <file>` (`-c`) picks one explicitly. All paths in it are\nrelative to the config file, and its directory becomes the root. Without a config file, the defaults apply with\n`--root` as the root. Every key is optional.\n\n```jsonc\n{\n \"$schema\": \"./node_modules/domaintrail/schema/domaintrail.schema.json\",\n // globs of DSL files; \"!\" excludes (node_modules, .git, .domaintrail are never scanned)\n \"sources\": [\"domain/**/*.dt\", \"!domain/drafts/**\"], // default: [\"**/*.dt\"]\n // skip what .gitignore ignores when finding sources\n \"respectGitignore\": true, // default: true\n // print a hint on generate when no lint rule against casts to domain types is found (dt guide lint)\n \"lintHint\": true, // default: true\n \"generate\": {\n \"ts\": {\n \"out\": \"src/domain/generated\", // default: \"generated/domain\"\n // policies: \"optional\" (a handler type each) or \"required\" (also <Context>PolicyHandlers, which the contract tests take)\n \"policies\": \"optional\", // default: \"optional\"\n // per context: a directory of its own instead of \"out\", and/or its own \"policies\" (both optional)\n \"contexts\": { \"Common\": { \"out\": \"packages/shared/src/generated\" }, \"Billing\": { \"policies\": \"required\" } },\n // contract tests, written only with this key (optional); needs fast-check in your project\n \"tests\": { \"out\": \"src/domain/generated/tests\", \"runner\": \"vitest\" } // default out: <out>/tests; runner: bun, vitest, jest, node\n }\n },\n \"view\": { \"port\": 4321, \"host\": \"127.0.0.1\" } // defaults shown\n}\n```\n\nCLI options win over the file: `generate ts --out`, `view --port`, `view --host`. `--out` replaces `generate.ts.out`\nonly; contexts listed in `generate.ts.contexts` keep their own directories. Sources and output directories, `--out`\nincluded, must stay inside the config file's directory (the root without a config file): an absolute path or one with\n`..` is a config error, and an output directory reached through a symlink stops generation. A name in\n`generate.ts.contexts` that is not a context of the model is a config error, reported by `generate ts` once the model\nhas been checked. Without `generate.ts.tests.out`, the tests follow `--out` (`<out>/tests`). The policies of a context\nare `generate.ts.contexts.<Context>.policies` if given, else `generate.ts.policies`, else `optional` (see\n[Reactions to events](#reactions-to-events-policies)).\n\n**Sources and `.gitignore`.** The sources are the files below the root that match `sources`, outside `node_modules`,\n`.git` and `.domaintrail`, and not ignored by git: every command that finds them (`check`, `generate`, `view`, `fmt`,\n`show`, `lsp`) skips the files and directories that the `.gitignore` files ignore, with git's rules (`!` negation,\n`dir/` for directories only, a leading or inner `/` anchoring to the file's directory, a deeper file winning). The\n`.gitignore` files that count are those of the root and its subdirectories, those of the directories above the root up\nto the top of the enclosing git repository, and the repository's `.git/info/exclude`; without a repository, only those\nfrom the root down (a `.gitignore` counts whether or not the directory is a repository). A root that the rules above it\nignore (a copy of a project in an ignored directory, checked with `-r`) is still checked, with the `.gitignore` files\nfrom the root down only. So a generated or cloned copy of the model in an ignored directory (an editor's grammar\ncheckout, a build output) is not checked twice. `\"respectGitignore\": false` turns this off; files named on the command\nline (`dt fmt <files>`) are never skipped.\n\n<!-- topic: lsp -->\n## Language server\n\n`dt lsp` is a language server (LSP) for `.dt` files, speaking over stdin/stdout. Editors and AI agents start it; it\nruns until the client shuts it down. It offers:\n\n- **Diagnostics equal to `dt check`**: the same rules, severities, codes (the rule ID, e.g. `M009`) and messages,\n computed over the whole workspace (the config's `sources`) with the unsaved text of open files, after every change.\n A hint follows the message on a second line (`hint: did you mean 'Confirmed'?`) and is also in the diagnostic's\n `data.hint`.\n- **Go to definition** and **find references** for types, states, events, errors, field sets and the commands of\n policies, across files and contexts (`Sales.Order` included).\n- **Hover**: kind, name, Japanese name, `///` description, the expression of an invariant or requirement, the\n `emits` entries of a command and the policies that run it, the reaction of a policy, and the policies that react to\n an event.\n- **Outline** (document symbols), **completion** of names, and **rename** of context elements, states and commands\n (a command is referenced by policies), which updates every reference (a qualified `Sales.Order` keeps its context).\n Contexts, fields and parameters are not renamed, since their names also appear outside references (in qualified\n names, expressions and terms).\n\nThe workspace is the config found from `--root` or `--config` when given, else from the client's (first) workspace\nfolder, else from the current directory, exactly as `dt check` would find it there (with the same sources, `.gitignore`\nincluded). When that search finds no config file, the first `.dt` file opened below that directory whose directory or\na parent (up to it) has a `domaintrail.jsonc` / `domaintrail.json` makes that config the workspace: its directory is\nthe root, its `sources` are loaded, and the files no longer in the workspace lose their diagnostics. So opening a\nrepository with the model in `examples/shop/` checks that model as `dt -r examples/shop check` does. A session has one\nworkspace: a file opened later under another config file gets no diagnostics, and a warning says so once (open that\ndirectory as its own project to check it). An open file outside the workspace's sources gets no diagnostics either.\n\nA client needs only: command `dt lsp` (with `dt` on the `PATH`, or e.g. `npx dt lsp`), transport stdio, file\nextension `.dt`. For Claude Code, a plugin declares it in an `.lsp.json` at the plugin root (load it with\n`claude --plugin-dir <plugin directory>`); Claude then sees the diagnostics after each edit of a `.dt` file:\n\n```json\n{\n \"domaintrail\": {\n \"command\": \"dt\",\n \"args\": [\"lsp\"],\n \"extensionToLanguage\": { \".dt\": \"domaintrail\" }\n }\n}\n```\n\n<!-- topic: editors -->\n## Editors\n\n**Zed.** The extension in `editors/zed` of the repository highlights `.dt` files with the tree-sitter grammar in\n`tree-sitter-domaintrail`, gives brackets, indentation and an outline (contexts, their declarations, the states and\ncommands of aggregates), and starts `dt lsp` (diagnostics, navigation, hover, rename, formatting; see [Language\nserver](#language-server)). Install it with *zed: install dev extension* and the `editors/zed` directory of a clone\n(it needs Rust, installed with rustup). The language server is the first of: `lsp.domaintrail.binary` in Zed's\nsettings, `dt` on the `PATH`, else domaintrail at the version the extension pins, installed into the extension's own\ndirectory and run with Zed's Node. By default the extension runs no file of the project (such as its\n`node_modules/.bin/dt`); to use the project's own `dt`, name it in the settings. Zed merges a project's\n`.zed/settings.json` into these settings, so a project can name a command too, as for Zed's built-in language servers:\nopen untrusted repositories in Zed's restricted mode.\n\n```json\n{ \"lsp\": { \"domaintrail\": { \"binary\": { \"path\": \"/path/to/dt\", \"arguments\": [\"lsp\"] } } } }\n```\n\nOpen the directory with the `domaintrail.jsonc` as the project, or a directory above it: then the first `.dt` file\nopened picks the nearest config (see [Language server](#language-server)), provided no config is found from the\nproject directory itself. Directories that `.gitignore` ignores, such as the grammar checkout Zed keeps in\n`editors/zed/grammars` when the extension is installed from a clone, are not part of the workspace.\n\n**Other editors.** Any LSP client works with command `dt lsp`, transport stdio, file extension `.dt` (see\n[Language server](#language-server)). Editors that highlight with tree-sitter (Neovim, Helix, Emacs) can use the\ngrammar in `tree-sitter-domaintrail`.\n";
|
|
13569
13877
|
|
|
13570
13878
|
// src/cli/main.ts
|
|
13571
13879
|
var defaultIO = {
|
|
@@ -13680,29 +13988,29 @@ Examples:
|
|
|
13680
13988
|
return;
|
|
13681
13989
|
}
|
|
13682
13990
|
checkTsContexts(cfg, a.model.contexts);
|
|
13683
|
-
const defaultOut =
|
|
13684
|
-
const outOf = (ctx) => (ctx in cfg.tsContexts) ?
|
|
13685
|
-
const testsOut = cfg.tsTests &&
|
|
13686
|
-
for (const dir of new Set([defaultOut, ...Object.values(cfg.tsContexts).map((d) =>
|
|
13991
|
+
const defaultOut = path10.resolve(cfg.root, opts.out ?? cfg.tsOut);
|
|
13992
|
+
const outOf = (ctx) => (ctx in cfg.tsContexts) ? path10.resolve(cfg.root, cfg.tsContexts[ctx]) : defaultOut;
|
|
13993
|
+
const testsOut = cfg.tsTests && path10.resolve(cfg.root, cfg.tsTests.out ?? path10.join(opts.out ?? cfg.tsOut, "tests"));
|
|
13994
|
+
for (const dir of new Set([defaultOut, ...Object.values(cfg.tsContexts).map((d) => path10.resolve(cfg.root, d)), ...testsOut ? [testsOut] : []])) {
|
|
13687
13995
|
const linked = await symlinkOnPath(cfg.root, dir);
|
|
13688
13996
|
if (linked) {
|
|
13689
13997
|
fail(`Generation skipped: ${linked} is a symlink; an output directory must be a plain directory inside ${cfg.root}.`, 1);
|
|
13690
13998
|
return;
|
|
13691
13999
|
}
|
|
13692
14000
|
}
|
|
13693
|
-
const dirs = Object.fromEntries(a.model.contexts.map((c) => [c, toPosix(
|
|
13694
|
-
const tests = testsOut ? { dir: toPosix(
|
|
14001
|
+
const dirs = Object.fromEntries(a.model.contexts.map((c) => [c, toPosix(path10.relative(cfg.root, outOf(c)))]));
|
|
14002
|
+
const tests = testsOut ? { dir: toPosix(path10.relative(cfg.root, testsOut)), ...cfg.tsTests?.runner && { runner: cfg.tsTests.runner } } : undefined;
|
|
13695
14003
|
const files = generateTypeScript(a.model, dirs, tests, policyModes(cfg, a.model.contexts)).map((f) => ({
|
|
13696
14004
|
...f,
|
|
13697
|
-
file:
|
|
14005
|
+
file: path10.resolve(cfg.root, f.path)
|
|
13698
14006
|
}));
|
|
13699
14007
|
const foreign = await foreignFiles(files.map((f) => f.file));
|
|
13700
14008
|
if (foreign.length) {
|
|
13701
|
-
const names = foreign.map((f) =>
|
|
14009
|
+
const names = foreign.map((f) => path10.relative(process.cwd(), f) || f);
|
|
13702
14010
|
fail(`Generation skipped: ${names.join(", ")} ${foreign.length > 1 ? "are" : "is"} not generated by domaintrail and would be overwritten.`, 1);
|
|
13703
14011
|
return;
|
|
13704
14012
|
}
|
|
13705
|
-
const outDirs = [...new Set(files.map((f) =>
|
|
14013
|
+
const outDirs = [...new Set(files.map((f) => path10.dirname(f.file)))];
|
|
13706
14014
|
const previous = await readManifest(cfg.root);
|
|
13707
14015
|
await removeStale(cfg.root, previous, new Set(files.map((f) => f.file)));
|
|
13708
14016
|
for (const dir of outDirs)
|
|
@@ -13712,7 +14020,7 @@ Examples:
|
|
|
13712
14020
|
const recorded = await writeManifest(cfg.root, files.map((f) => f.path));
|
|
13713
14021
|
if (!recorded)
|
|
13714
14022
|
io.err(`Warning: generated files not recorded in ${MANIFEST_FILE}, which is not a plain file in a plain directory; they will not be removed automatically.`);
|
|
13715
|
-
const written = outDirs.map((d) =>
|
|
14023
|
+
const written = outDirs.map((d) => path10.relative(process.cwd(), d) || ".");
|
|
13716
14024
|
const hints = cfg.lintHint && !await hasCastLintRule(cfg.root) ? [LINT_HINT] : [];
|
|
13717
14025
|
if (report) {
|
|
13718
14026
|
report.written = written;
|
|
@@ -13740,10 +14048,10 @@ Examples:
|
|
|
13740
14048
|
dt -r domain fmt`).action(async (files, opts) => {
|
|
13741
14049
|
let entries;
|
|
13742
14050
|
if (files.length)
|
|
13743
|
-
entries = files.map((f) => ({ path: f, file:
|
|
14051
|
+
entries = files.map((f) => ({ path: f, file: path10.resolve(f) }));
|
|
13744
14052
|
else {
|
|
13745
14053
|
const cfg = await config();
|
|
13746
|
-
entries = (await findDomainFiles(cfg.root, cfg.sources, cfg.respectGitignore)).map((f) => ({ path: f, file:
|
|
14054
|
+
entries = (await findDomainFiles(cfg.root, cfg.sources, cfg.respectGitignore)).map((f) => ({ path: f, file: path10.join(cfg.root, f) }));
|
|
13747
14055
|
if (!entries.length)
|
|
13748
14056
|
throw new CliError(`No .dt files found under ${cfg.root} (sources: ${cfg.sources.join(", ")}).`);
|
|
13749
14057
|
}
|
|
@@ -13771,16 +14079,18 @@ Examples:
|
|
|
13771
14079
|
(broken.size || opts.check && changed ? io.err : io.out)(summary);
|
|
13772
14080
|
exitCode = broken.size || opts.check && changed ? 1 : 0;
|
|
13773
14081
|
});
|
|
13774
|
-
program.command("view").description("Start the Live View server and watch .dt files (runs until Ctrl+C).").option("-p, --port <port>", "port to listen on (default: view.port, else 4321)", parsePort).option("--host <host>", "host to bind (default: view.host, else 127.0.0.1)").addHelpText("after", `
|
|
14082
|
+
program.command("view").description("Start the Live View server and watch .dt files (runs until Ctrl+C).").option("-p, --port <port>", "port to listen on (default: view.port, else 4321)", parsePort).option("--host <host>", "host to bind (default: view.host, else 127.0.0.1)").option("--base <ref>", "compare the model with this git revision (e.g. main, HEAD~3) instead of the previous save").addHelpText("after", `
|
|
13775
14083
|
A server that keeps running and never exits by itself: stop it with Ctrl+C. It re-checks on every save and
|
|
13776
14084
|
pushes updates to the browser. Use \`dt check\` for a one-shot result (e.g. from scripts or AI agents).
|
|
13777
14085
|
|
|
13778
14086
|
Examples:
|
|
13779
14087
|
dt view
|
|
13780
14088
|
dt view --port 8080
|
|
14089
|
+
dt view --base main mark what changed since main (the base can be switched in the page)
|
|
13781
14090
|
dt -r examples/shop view`).action(async (opts) => {
|
|
13782
14091
|
const cfg = await config();
|
|
13783
14092
|
await Promise.resolve().then(() => init_server());
|
|
14093
|
+
await Promise.resolve().then(() => init_git());
|
|
13784
14094
|
const port = opts.port ?? cfg.view.port;
|
|
13785
14095
|
const host = opts.host ?? cfg.view.host;
|
|
13786
14096
|
const server = await startViewServer({
|
|
@@ -13789,14 +14099,17 @@ Examples:
|
|
|
13789
14099
|
respectGitignore: cfg.respectGitignore,
|
|
13790
14100
|
port,
|
|
13791
14101
|
host,
|
|
14102
|
+
base: opts.base,
|
|
13792
14103
|
log: io.out
|
|
13793
14104
|
}).catch((e) => {
|
|
14105
|
+
if (e instanceof UnknownRevisionError)
|
|
14106
|
+
throw new CliError(`${e.message}: pass a branch, tag or commit to --base.`);
|
|
13794
14107
|
if (errorCode(e) === "EADDRINUSE")
|
|
13795
14108
|
throw new CliError(`Port ${port} on ${host} is already in use; pick another with --port.`);
|
|
13796
14109
|
throw e;
|
|
13797
14110
|
});
|
|
13798
14111
|
if (cfg.file)
|
|
13799
|
-
io.out(`Config: ${
|
|
14112
|
+
io.out(`Config: ${path10.relative(process.cwd(), cfg.file) || cfg.file}`);
|
|
13800
14113
|
io.out(`Live View: ${server.url}`);
|
|
13801
14114
|
await new Promise(() => {});
|
|
13802
14115
|
});
|
|
@@ -13810,7 +14123,7 @@ there, the nearest config of the first .dt file opened below it. Started by an L
|
|
|
13810
14123
|
Examples:
|
|
13811
14124
|
dt lsp
|
|
13812
14125
|
dt -r domain lsp`).action(async () => {
|
|
13813
|
-
const root = program.getOptionValueSource("root") === "default" ? undefined :
|
|
14126
|
+
const root = program.getOptionValueSource("root") === "default" ? undefined : path10.resolve(program.opts().root);
|
|
13814
14127
|
if (root !== undefined || program.opts().config)
|
|
13815
14128
|
await config();
|
|
13816
14129
|
await Promise.resolve().then(() => init_server2());
|
|
@@ -13887,7 +14200,7 @@ Examples:
|
|
|
13887
14200
|
io.out(found.text);
|
|
13888
14201
|
});
|
|
13889
14202
|
const config = async () => {
|
|
13890
|
-
const start =
|
|
14203
|
+
const start = path10.resolve(program.opts().root);
|
|
13891
14204
|
const info = await stat(start).catch(() => {
|
|
13892
14205
|
return;
|
|
13893
14206
|
});
|
|
@@ -13942,12 +14255,12 @@ async function isGeneratedOrMissing(file) {
|
|
|
13942
14255
|
return !stat || stat.isFile() && (await readFile6(file, "utf8")).startsWith(GENERATED_MARKER);
|
|
13943
14256
|
}
|
|
13944
14257
|
async function symlinkOnPath(root, target) {
|
|
13945
|
-
const rel =
|
|
13946
|
-
if (rel.startsWith("..") ||
|
|
14258
|
+
const rel = path10.relative(root, target);
|
|
14259
|
+
if (rel.startsWith("..") || path10.isAbsolute(rel))
|
|
13947
14260
|
return target;
|
|
13948
14261
|
let current = root;
|
|
13949
|
-
for (const part of rel.split(
|
|
13950
|
-
current =
|
|
14262
|
+
for (const part of rel.split(path10.sep).filter(Boolean)) {
|
|
14263
|
+
current = path10.join(current, part);
|
|
13951
14264
|
const stat = await lstat2(current).catch(() => {
|
|
13952
14265
|
return;
|
|
13953
14266
|
});
|
|
@@ -13959,7 +14272,7 @@ async function symlinkOnPath(root, target) {
|
|
|
13959
14272
|
return;
|
|
13960
14273
|
}
|
|
13961
14274
|
async function writeNew(file, content) {
|
|
13962
|
-
const temp =
|
|
14275
|
+
const temp = path10.join(path10.dirname(file), `.${path10.basename(file)}.${process.pid}.tmp`);
|
|
13963
14276
|
await writeFile2(temp, content, { flag: "wx" });
|
|
13964
14277
|
await rename2(temp, file);
|
|
13965
14278
|
}
|
|
@@ -13968,23 +14281,23 @@ async function cleanGenerated(outDir) {
|
|
|
13968
14281
|
for (const entry of await readdir3(outDir, { withFileTypes: true })) {
|
|
13969
14282
|
if (!entry.isFile() || !entry.name.endsWith(".ts"))
|
|
13970
14283
|
continue;
|
|
13971
|
-
const file =
|
|
14284
|
+
const file = path10.join(outDir, entry.name);
|
|
13972
14285
|
if ((await readFile6(file, "utf8")).startsWith(GENERATED_MARKER))
|
|
13973
14286
|
await rm(file);
|
|
13974
14287
|
}
|
|
13975
14288
|
}
|
|
13976
|
-
var MANIFEST_FILE =
|
|
14289
|
+
var MANIFEST_FILE = path10.join(".domaintrail", "generated.json");
|
|
13977
14290
|
async function readManifest(root) {
|
|
13978
14291
|
try {
|
|
13979
|
-
const m = JSON.parse(await readFile6(
|
|
14292
|
+
const m = JSON.parse(await readFile6(path10.join(root, MANIFEST_FILE), "utf8"));
|
|
13980
14293
|
return m.version === 1 && Array.isArray(m.files) ? m.files.filter((f) => typeof f === "string" && f !== "" && staysInside(f)) : [];
|
|
13981
14294
|
} catch {
|
|
13982
14295
|
return [];
|
|
13983
14296
|
}
|
|
13984
14297
|
}
|
|
13985
14298
|
async function writeManifest(root, files) {
|
|
13986
|
-
const target =
|
|
13987
|
-
const dir =
|
|
14299
|
+
const target = path10.join(root, MANIFEST_FILE);
|
|
14300
|
+
const dir = path10.dirname(target);
|
|
13988
14301
|
const existing = await lstat2(dir).catch(() => {
|
|
13989
14302
|
return;
|
|
13990
14303
|
});
|
|
@@ -13998,10 +14311,10 @@ async function writeManifest(root, files) {
|
|
|
13998
14311
|
}
|
|
13999
14312
|
async function removeStale(root, previous, written) {
|
|
14000
14313
|
for (const rel of previous) {
|
|
14001
|
-
const file =
|
|
14314
|
+
const file = path10.resolve(root, rel);
|
|
14002
14315
|
if (written.has(file) || !file.endsWith(".ts") || !staysInside(rel) || !isInside(root, file))
|
|
14003
14316
|
continue;
|
|
14004
|
-
const dir =
|
|
14317
|
+
const dir = path10.dirname(file);
|
|
14005
14318
|
if (await symlinkOnPath(root, dir))
|
|
14006
14319
|
continue;
|
|
14007
14320
|
const stat = await lstat2(file).catch(() => {
|
|
@@ -14015,10 +14328,10 @@ async function removeStale(root, previous, written) {
|
|
|
14015
14328
|
}
|
|
14016
14329
|
}
|
|
14017
14330
|
var isInside = (root, p) => {
|
|
14018
|
-
const rel =
|
|
14019
|
-
return rel !== ".." && !rel.startsWith(`..${
|
|
14331
|
+
const rel = path10.relative(root, p);
|
|
14332
|
+
return rel !== ".." && !rel.startsWith(`..${path10.sep}`) && !path10.isAbsolute(rel);
|
|
14020
14333
|
};
|
|
14021
|
-
var toPosix = (p) => p.split(
|
|
14334
|
+
var toPosix = (p) => p.split(path10.sep).join("/");
|
|
14022
14335
|
|
|
14023
14336
|
// bin/dt.ts
|
|
14024
14337
|
process.exitCode = await runCli(process.argv.slice(2));
|