d2-diagram

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

d2-diagram

d2-diagram

Paths below:
<skill>
is this skill's directory and
<skills>
the directory the skills are installed in (
skills/
in mizchi/explainer,
.claude/skills/
in a project that installed them).
A D2 file is the diagram; the picture is a render of it. The writer's job is the text — ids, labels, containers, connections — and TALA's job is where the boxes go and how the lines bend. The terminal render exists so the writer reads the picture on every round without leaving the shell, and so the diagram can live in a README code block, a PR comment, or a
--help
text.
d2
direction: right
web: Web frontend
api: API server
db: PostgreSQL {shape: cylinder}
web -> api: HTTPS
api -> db: SQL
                                                          .-‾‾‾‾‾‾-.
                                                         │╲-______-╱│
                    HTTPS                        SQL     │          │
┌─────────────┐──────────────▶┌───────────┐─────────────▶│          │
│Web frontend │               │API server │              │PostgreSQL│
│             │               │           │              │          │
└─────────────┘               └───────────┘               ╲-______-╱
(
d2 --layout=tala x.d2 x.txt
, verbatim, D2 0.8.1-HEAD.)
This skill is for a diagram whose deliverable is D2 text: someone will edit it later, TALA lays it out, and it renders wherever D2 does. A step-by-step walk (an algorithm, a request) is not animated: draw the structure once and number the steps in the prose. To ground the D2 in the code, list the modules and dependencies from the code first (imports,
package.json
), write them into the fact sheet, and draw exactly those.
下文路径:
<skill>
表示本 skill 的目录,
<skills>
表示 skill 的安装目录(在 mizchi/explainer 中为
skills/
,在安装了这些 skill 的项目中为
.claude/skills/
)。
D2 文件才是图本身;图片是它的 render。编写者负责文本——id、label、container、connection——TALA 则负责 box 的位置和连线的弯折方式。terminal render 让编写者每轮都无需离开 shell 就能阅读图表,也让图表可以出现在 README code block、PR comment 或
--help
文本中。
d2
direction: right
web: Web frontend
api: API server
db: PostgreSQL {shape: cylinder}
web -> api: HTTPS
api -> db: SQL
                                                          .-‾‾‾‾‾‾-.
                                                         │╲-______-╱│
                    HTTPS                        SQL     │          │
┌─────────────┐──────────────▶┌───────────┐─────────────▶│          │
│Web frontend │               │API server │              │PostgreSQL│
│             │               │           │              │          │
└─────────────┘               └───────────┘               ╲-______-╱
(
d2 --layout=tala x.d2 x.txt
,逐字输出,D2 0.8.1-HEAD。)
本 skill 适用于最终交付物是 D2 文本的图表:后续会有人编辑它,由 TALA 负责布局,并且可以在 D2 能 render 的任何位置使用。逐步流程(算法、请求)不需要动画:一次画出结构,并在正文中为步骤编号。为了让 D2 忠实反映代码,先从代码中列出 module 和 dependency(imports、
package.json
),将它们写入 fact sheet,然后严格按这些内容绘图。

Invocation

调用方式

bash
d2 layout                                   # must list `tala (bundled)`. That, not the version
                                            # string, is the gate: v0.8.1-HEAD already bundles it.

d2 --layout=tala arch.d2 arch.txt && cat arch.txt        # the terminal render (Unicode box drawing)
d2 --layout=tala --ascii-mode standard arch.d2 arch.txt  # plain ASCII (+ - | / < > v) for a README
d2 --layout=tala arch.d2 --stdout-format ascii -         # to stdout, for a pipe or a PR comment
d2 --layout=tala arch.d2 arch.svg                        # the figure for docs
d2 --layout=tala arch.d2 arch.png                        # a raster to LOOK at (see the loop, step 5)

d2 validate arch.d2                         # syntax only; exit 1 with file:line:col
d2 fmt --check arch.d2                      # formatted? exit 1 and names the file, changes nothing
d2 fmt arch.d2                              # ...and this rewrites it in place

node <skill>/assets/d2-facts.mjs arch.d2               # what the picture DRAWS
node <skill>/assets/d2-facts.mjs arch.d2 --expect f.json   # ...held to a sheet
LC_ALL=C.UTF-8 wc -L arch.txt               # the width in columns — the locale matters, see below
Install when
d2
is missing — never drive the diagram through an online playground:
bash
curl -fsSL https://d2lang.com/install.sh | sh -s --      # the installer; it rejects the obsolete --tala flag
brew install d2                                          # macOS
go install github.com/d2lang/d2@latest                   # Go 1.27+; what to do when the install script gets a 403 through a proxy
D2_LAYOUT=tala
in the environment is the same as the flag.
vars: { d2-config: { layout-engine: tala } }
inside the file is honoured for
.svg
/
.png
and not for
.txt
on the build measured (0.8.1-HEAD: the text render fell back to dagre and came out 163 columns instead of 120), so pass the flag anyway; keep the config in the file so the next person's SVG gets the same engine. A
sequence_diagram
is the exception that proves the rule — it lays itself out, so its text render came out byte-identical with and without the flag.
bash
d2 layout                                   # must list `tala (bundled)`. That, not the version
                                            # string, is the gate: v0.8.1-HEAD already bundles it.

d2 --layout=tala arch.d2 arch.txt && cat arch.txt        # the terminal render (Unicode box drawing)
d2 --layout=tala --ascii-mode standard arch.d2 arch.txt  # plain ASCII (+ - | / < > v) for a README
d2 --layout=tala arch.d2 --stdout-format ascii -         # to stdout, for a pipe or a PR comment
d2 --layout=tala arch.d2 arch.svg                        # the figure for docs
d2 --layout=tala arch.d2 arch.png                        # a raster to LOOK at (see the loop, step 5)

d2 validate arch.d2                         # syntax only; exit 1 with file:line:col
d2 fmt --check arch.d2                      # formatted? exit 1 and names the file, changes nothing
d2 fmt arch.d2                              # ...and this rewrites it in place

node <skill>/assets/d2-facts.mjs arch.d2               # what the picture DRAWS
node <skill>/assets/d2-facts.mjs arch.d2 --expect f.json   # ...held to a sheet
LC_ALL=C.UTF-8 wc -L arch.txt               # the width in columns — the locale matters, see below
缺少
d2
时应进行安装——绝不要通过在线 playground 驱动图表:
bash
curl -fsSL https://d2lang.com/install.sh | sh -s --      # the installer; it rejects the obsolete --tala flag
brew install d2                                          # macOS
go install github.com/d2lang/d2@latest                   # Go 1.27+; what to do when the install script gets a 403 through a proxy
环境变量中的
D2_LAYOUT=tala
与该 flag 等效。文件内的
vars: { d2-config: { layout-engine: tala } }
在所测版本中会对
.svg
/
.png
生效,但对
.txt
不生效
(0.8.1-HEAD:text render 回退到 dagre,宽度为 163 列,而不是 120 列),因此仍应传入 flag;同时保留文件中的配置,以便下一位使用者生成 SVG 时使用同一引擎。
sequence_diagram
是证明该规律的例外——它会自行布局,因此无论是否传入该 flag,其 text render 都逐字节一致。

Nothing in D2 tells you the picture is wrong

D2 本身无法告诉你图表哪里错了

validate
reads syntax. The renderers draw whatever the file says. There is no
--expect
, no layout report, and no error for a reference to an id that is not in scope — it creates a new shape. So this is a silently wrong diagram:
d2
edge:    { cdn; gateway }
cluster: { orders; inventory }
gateway -> orders        # ← two NEW empty boxes at the root; the real ones keep no arrow
A writer in this skill's own validation round shipped exactly that:
d2 validate
0,
d2 fmt
clean, 94 columns, and its log said "can trace all connections". The render had four duplicated boxes, two orphans, and the system's entry call drawn as a floating pair outside every region (report).
Write cross-container connections at the root, with full paths. That is the one form that always works and the one every writer in the round that got this right used:
d2
edge.gateway -> cluster.orders          # at the root: full path on both ends
cluster: {
  billing -> _.outside.stripe           # from INSIDE: `_` is this container's parent
}
_
is one level and only valid inside a container. From inside
cluster
,
_
is the root, so a shape in a sibling container is
_.outside.stripe
— not
_.stripe
, which invents a root-level
stripe
(that was the round's fourth phantom). At the root
_
has no parent to mean:
d2
fails the compile with
invalid underscore
, which a later writer hit and read as the escape being broken.
assets/d2-facts.mjs
is the check D2 does not have. It renders the file and reads the picture back out of the SVG — d2 writes every shape's and every connection's fully-qualified id into it — so what it reports is what a reader sees, not what the source seems to say. No dependencies; copy it next to your diagrams in another repo.
bash
node assets/d2-facts.mjs arch.d2               # boxes, containers and their members, labels, edges, columns
node assets/d2-facts.mjs arch.d2 --expect arch.facts.json
node assets/d2-facts.mjs arch.d2 --seeds 4,5,6 # the seed the final render will use, so the width it reports is real
node assets/d2-facts.mjs --from-svg arch.svg --from-txt arch.txt   # read renders that already exist, no d2 run
The last form is how the checker is itself gated:
tests/d2/d2-facts.test.mjs
drives it over renders committed under
tests/d2/fixtures/d2-scenario/
with no
d2
installed, so the phantom-box, truncation, reversed-edge and width lines each have a test that fails if they stop firing. It is also the way to check a render somebody else produced, or a
.svg
in a PR you are reviewing.
It fails on its own, with no sheet, for three defects that are always defects: a name drawn twice (the scoping trap above), a box that overlaps a sibling or escapes its container, and a truncated terminal render — a
top
/
left
pin can push shapes off the ascii canvas, and
d2
exits 0 on a text file holding a fragment. One writer's five-table schema passed at "71 columns" with three of the five tables missing from the render it had just measured, which is why the box-by-box comparison exists. It warns about a box nothing connects to. With a sheet it also checks:
json
{
  "boxes": ["gateway", "orders", "broker"],
  "deps": ["gateway->orders", "orders->broker"],
  "forbidden": ["orders->billing"],
  "containers": { "cluster": ["orders", "inventory"] },
  "order": ["browser->gateway", "gateway->orders"],
  "exhaustive": true,
  "maxColumns": 100,
  "allowOrphans": ["legend"]
}
  • deps
    are checked with direction: a reversed arrow reads
    ✗ edge reversed: the sheet says broker->billing, the picture draws billing->broker
    .
  • exhaustive
    makes an edge the sheet does not list an error, not a warning.
  • order
    reads a sequence diagram's messages back by their y position — the one place D2 makes order visible.
  • Names match an id's last segment or its label, so a sheet says
    gateway
    for a file that wrote
    gw: API gateway
    .
  • It cannot see inside a
    sql_table
    : a column-level edge (
    orders.customer_id -> customers.id
    ) is reported as
    orders->customers
    . Constraint badges are not checked either; count them with
    grep -o '>FK</text>' arch.svg | wc -l
    (
    grep -c
    counts lines, and an SVG is one line, so it prints 1 however many badges there are) and read the PNG for which row each arrow actually leaves.
Write the sheet from the brief before the diagram, the way a test comes first. It is the only artifact that survives a TALA reflow.
validate
只检查语法。renderer 会绘制文件中写下的一切。D2 没有
--expect
,没有 layout report,并且不会对超出作用域的 id 引用报错——而是创建一个新 shape。例如下面就是一个静默错误的图表:
d2
edge:    { cdn; gateway }
cluster: { orders; inventory }
gateway -> orders        # ← two NEW empty boxes at the root; the real ones keep no arrow
本 skill 自身验证轮次中的一位编写者交付的正是这种图:
d2 validate
返回 0,
d2 fmt
无问题,宽度为 94 列,日志还声称“can trace all connections”。但 render 中包含四个重复 box、两个孤立节点,系统入口调用则被画成悬浮在所有区域之外的一对节点(报告)。
在 root 层级使用完整路径编写跨 container 的 connection。 这是始终有效的唯一写法,也是本轮所有编写正确图的人所采用的写法:
d2
edge.gateway -> cluster.orders          # at the root: full path on both ends
cluster: {
  billing -> _.outside.stripe           # from INSIDE: `_` is this container's parent
}
_
仅表示一级,且只能在 container 内部使用。在
cluster
内部,
_
指向 root,因此 sibling container 中的 shape 应写为
_.outside.stripe
,而不是
_.stripe
;后者会凭空创建一个 root 层级的
stripe
(那就是本轮出现的第四个幽灵节点)。在 root 层级,
_
没有可表示的父级:
d2
编译时会报
invalid underscore
;后来一位编写者遇到该错误,却误以为是转义写法失效了。
assets/d2-facts.mjs
是 D2 不具备的检查。它会 render 文件,并从 SVG 中反向读出图表——D2 会把每个 shape 和 connection 的完全限定 id 写入 SVG——因此它报告的是读者实际看到的内容,而不是源码表面上的含义。它不依赖任何 dependency;在其他 repo 中使用时,将其复制到图表旁边。
bash
node assets/d2-facts.mjs arch.d2               # boxes, containers and their members, labels, edges, columns
node assets/d2-facts.mjs arch.d2 --expect arch.facts.json
node assets/d2-facts.mjs arch.d2 --seeds 4,5,6 # the seed the final render will use, so the width it reports is real
node assets/d2-facts.mjs --from-svg arch.svg --from-txt arch.txt   # read renders that already exist, no d2 run
最后一种形式也用于为 checker 自身建立测试门槛:在未安装
d2
的情况下,
tests/d2/d2-facts.test.mjs
会对提交在
tests/d2/fixtures/d2-scenario/
下的 render 执行检查,因此幽灵 box、截断、edge 反向和宽度检查各自都有测试,若不再触发便会失败。也可以用这种方式检查他人生成的 render,或你正在评审的 PR 中的
.svg
。
即使没有 fact sheet,它也会针对三种始终属于缺陷的问题失败:同名 shape 被绘制两次(上述作用域陷阱)、box 与 sibling 重叠或逃出 container,以及terminal render 被截断——
top
/
left
pin 可能把 shape 推出 ASCII canvas,而对于只包含残片的 text 文件,
d2
仍会以 0 退出。一位编写者的五表 schema 在“71 列”处通过检查,但其刚测量的 render 中五张表里有三张缺失,因此才有逐 box 对比。它会对没有任何连接的 box 发出警告。
提供 fact sheet 后,它还会检查:
json
{
  "boxes": ["gateway", "orders", "broker"],
  "deps": ["gateway->orders", "orders->broker"],
  "forbidden": ["orders->billing"],
  "containers": { "cluster": ["orders", "inventory"] },
  "order": ["browser->gateway", "gateway->orders"],
  "exhaustive": true,
  "maxColumns": 100,
  "allowOrphans": ["legend"]
}
  • deps
    会按方向检查:反向 arrow 会显示为
    ✗ edge reversed: the sheet says broker->billing, the picture draws billing->broker
    。
  • exhaustive
    会将 fact sheet 未列出的 edge 视为错误,而不是警告。
  • order
    根据 sequence diagram message 的 y 坐标读取顺序——这是 D2 唯一能体现顺序的地方。
  • 名称可以匹配 id 的最后一段或其 label,因此文件写成
    gw: API gateway
    时,fact sheet 中仍可写
    gateway
    。
  • 它无法看穿
    sql_table
    内部:column 级 edge(
    orders.customer_id -> customers.id
    )会被报告为
    orders->customers
    。Constraint badge 也不会被检查;可用
    grep -o '>FK</text>' arch.svg | wc -l
    统计(
    grep -c
    统计的是行数,而 SVG 只有一行,因此无论 badge 数量是多少都输出 1),并查看 PNG 以确认每个 arrow 实际从哪一行离开。
在绘图之前,先根据需求编写 fact sheet,就像先写测试一样。它是 TALA 重新布局后仍能保留的唯一产物。

How a sheet name finds its shape

fact sheet 中的名称如何匹配 shape

A name in a sheet is not matched on the id alone; the search widens until exactly one shape matches:
  1. the last segment of an id —
    orders
    finds
    cluster.orders
  2. an exact label —
    gateway
    finds
    gw: API gateway
  3. a substring of an id, or of a label, when exactly one shape matches
Step 3 is generous on purpose and it will surprise you. A box called
pg
labelled
Postgres orders (authoritative until cutover)
is a sheet's
orders
, so
forbidden: ["checkout->orders"]
fires on an edge into it. That is not a bug in either the sheet or the figure — they disagree about a name — so every line that turns on a loose match now says which match it used:
✗ forbidden edge drawn: checkout->orders (orders matched no id — it is a
  substring of box pg's label "Postgres orders (authoritative until cutover)")
Read that clause before editing anything: with it, the choice is between scoping the sheet and fixing the figure. Without it, a writer relabelled a correct box to satisfy the checker (
tests/d2/fixtures/d2-scenario/v2/label-substring/
).
fact sheet 中的名称并非只按 id 匹配;搜索范围会逐步扩大,直到恰好匹配一个 shape:
  1. id 的最后一段——
    orders
    可找到
    cluster.orders
  2. 完全匹配的 label——
    gateway
    可找到
    gw: API gateway
  3. id 或 label 的子字符串,前提是恰好只有一个 shape 匹配
第 3 步有意采取宽松匹配,因此可能出乎意料。名为
pg
、label 为
Postgres orders (authoritative until cutover)
的 box 就是 fact sheet 中的
orders
,所以
forbidden: ["checkout->orders"]
会对指向它的 edge 触发。这不是 fact sheet 或图本身的 bug——二者对某个名称的理解不一致——因此现在每条依赖宽松匹配的检查结果都会说明具体采用了哪种匹配:
✗ forbidden edge drawn: checkout->orders (orders matched no id — it is a
  substring of box pg's label "Postgres orders (authoritative until cutover)")
编辑任何内容前先阅读这行说明:有了它,你可以在“收窄 fact sheet 的匹配范围”和“修正图表”之间作出选择;没有它,一位编写者为了满足 checker 而给一个正确的 box 改了 label(
tests/d2/fixtures/d2-scenario/v2/label-substring/
)。

Why TALA, and when not

为什么使用 TALA,以及何时不用

TALA (Terrastruct's AutoLayout Algorithm) is D2's own engine for software architecture diagrams, open-sourced under MPL-2.0 and bundled with D2 (0.9.0 is the release the announcement names; the 0.8.1-HEAD build measured here has it too — ask
d2 layout
, not the version). Where dagre and ELK lay a directed graph out in layers, TALA arranges boxes the way a whiteboard drawing does — orthogonal, symmetric where it can be, clustered by container — and it is the only engine that honours every D2 feature:
direction
per container,
width
/
height
on containers,
near: <other shape>
, and
top
/
left
to pin a shape while the engine places the rest.
The diagram is…Engine
Boxes in containers with connections in several directions (an architecture, a deployment, a data model)tala
One flow with a clear direction, many hops (a pipeline, a DAG)tala with
direction: right
; if it still reads badly,
elk
— the announcement is explicit that TALA does less well on flowing graphs
Thirty-plus shapesSplit by container into two files. TALA's runtime is nonlinear in size: 30 nodes took 0.94s here where ELK took 0.05s; layouts that size also read badly whatever the engine
Randomness, made reproducible. TALA tries three seeds (
--tala-seeds
, default
1,2,3
) and keeps the best complete result, so the same file renders the same picture twice — measured identical. A label change can still cascade into a different arrangement, so never describe the geometry in prose ("the box on the left"); name ids. Two measured caveats before you reach for seeds:
  • A different seed set often changes nothing. One writer swept twelve seed sets over a five-table schema and got the identical 109-column layout every time; another moved 113 → 100 columns with
    --tala-seeds 4,5,6
    . Try it once, keep the number you measured, and do not plan on it.
  • Seeds cannot live in the file.
    d2 layout tala
    says "Diagram data under tala-seeds takes precedence over the command-line flag", but
    vars.d2-config.tala-seeds
    is rejected at compile time. A committed diagram whose layout depends on a seed only renders that way if every render passes the flag — so write the flag into the script or the Makefile target, or do not rely on it.
TALA(Terrastruct's AutoLayout Algorithm)是 D2 自主开发的软件架构图布局引擎,以 MPL-2.0 开源,并随 D2 一起打包(公告中提到的是 0.9.0;这里实测的 0.8.1-HEAD 也包含它——应通过
d2 layout
确认,而不是看版本号)。dagre 和 ELK 会将 directed graph 分层布局,而 TALA 像白板绘图一样安排 box:正交对齐、在可行时保持对称,并按 container 聚类;它也是唯一支持 D2 全部以下特性的引擎:每个 container 的
direction
、container 上的
width
/
height
、
near: <other shape>
,以及在引擎安排其余 shape 时用
top
/
left
固定某个 shape。
图表属于……引擎
多个方向均有连接的 container 中的 box(架构、部署或数据模型)tala
方向明确、包含多个跳转的单一流(pipeline 或 DAG)tala,并设置
direction: right
;如果仍然难读,改用
elk
——公告明确指出 TALA 对 flowing graph 的表现较弱
超过 30 个 shape按 container 拆成两个文件。TALA 的运行时间随规模呈非线性增长:这里 30 个 node 用了 0.94 秒,而 ELK 只用了 0.05 秒;无论使用哪种引擎,这种规模的布局都不易阅读
让随机性可复现。 TALA 会尝试三个 seed(
--tala-seeds
,默认值为
1,2,3
)并保留最佳的完整结果,因此同一文件两次 render 会得到相同的图——实测逐字节一致。不过,改动 label 仍可能连锁导致布局变化,因此绝不要在正文中描述几何位置(“左侧的 box”),而应引用 id。在使用 seed 前,有两个经实测得到的注意事项:
  • 更换一组 seed 往往不会产生任何变化。 一位编写者针对五表 schema 测试了 12 组 seed,每次都得到完全相同的 109 列布局;另一位则通过
    --tala-seeds 4,5,6
    将宽度从 113 降到 100 列。可以试一次,但应以实测数字为准,不要依赖它必然生效。
  • Seed 不能写在文件里。
    d2 layout tala
    的提示为“Diagram data under tala-seeds takes precedence over the command-line flag”,但
    vars.d2-config.tala-seeds
    会在编译时被拒绝。若一个已提交的 diagram 布局依赖某个 seed,那么只有每次 render 都传入该 flag 才会得到相同布局——因此请把 flag 写进 script 或 Makefile target,否则不要依赖 seed。

Route by task

按任务类型选择写法

Task shapeWrite
"Draw the architecture / how the services fit"Containers per area (
cluster: our cluster { orders; inventory }
), connections between children at the root (
edge.gateway -> cluster.orders
) or with
_.
from inside.
direction: right
at the root; per-container
direction
on the fullest container is the width lever, see below
"Draw the data model / tables"
shape: sql_table
with
id: int {constraint: primary_key}
and
customer_id: int {constraint: foreign_key}
rows; connect the columns (
orders.customer_id -> customers.id
)
"Draw the request / call flow between components"
shape: sequence_diagram
at the root; actors in first-use order; one message per line with its label. D2 has no return or async arrow and the terminal render drops every style, so the distinction lives in the label — as a bare marker of four characters or fewer, no punctuation, at the start or the end (
ret 201 Created
,
order.placed async
), glossed in the prose above the figure.
reserved (ret)
does not survive: the render overwrites the parentheses with line characters and a label that fills the lane leaves the arrow with no head at all. A legend inside the diagram is not an option either —
shape: text
renders as an empty box
"Draw the classes"
shape: class
with
+method(): type
/
-field: type
lines
"Draw the deployment / network"Containers for hosts and zones;
shape: cylinder
for stores,
shape: cloud
for external services,
shape: queue
for brokers
"We already have a
.d2
"
Edit it;
d2 fmt --check
; render before and after; do not switch its engine or theme without saying so
"Put it in the README / a code block / a PR comment"The
standard
ASCII render inside a fenced block; measure the width as below; keep the
.d2
next to it
"It has to be checked against the code"List the modules and deps from the code (imports,
package.json
) into the sheet first, draw exactly those, then
d2-facts --expect
任务类型写法
“绘制架构/服务如何协作”按领域使用 container(
cluster: our cluster { orders; inventory }
),在 root 层级写 child 之间的 connection(
edge.gateway -> cluster.orders
),或从内部使用
_.
。root 使用
direction: right
;在最复杂的 container 上设置自身
direction
是控制宽度的手段,详见下文
“绘制数据模型/表结构”使用
shape: sql_table
,行中采用
id: int {constraint: primary_key}
和
customer_id: int {constraint: foreign_key}
;连接 column(
orders.customer_id -> customers.id
)
“绘制组件之间的请求/调用流程”在 root 使用
shape: sequence_diagram
;actor 按首次使用顺序排列;每行一条 message,并带 label。D2 没有 return 或 async arrow,terminal render 也会丢弃所有 style,因此区别要写在 label 中——在开头或结尾放置一个最多四个字符且不含标点的裸 marker(
ret 201 Created
、
order.placed async
),并在图示上方的正文中解释。
reserved (ret)
无法保留:render 会用线条字符覆盖括号,而填满 lane 的 label 会让 arrow 完全没有箭头头部。图内也不能放 legend:
shape: text
会 render 为空 box
“绘制类”使用
shape: class
,行内容采用
+method(): type
/
-field: type
“绘制部署/网络”为 host 和 zone 使用 container;存储使用
shape: cylinder
,外部服务使用
shape: cloud
,broker 使用
shape: queue
“已经有
.d2
”
编辑它;运行
d2 fmt --check
;分别 render 修改前后结果;除非先说明,否则不要切换 engine 或 theme
“放入 README/code block/PR comment”在 fenced block 中使用
standard
ASCII render;按下文方法测量宽度;将
.d2
放在同一位置
“需要对照代码检查”先将代码中的 module 和 dependency(imports、
package.json
)写入 fact sheet,再严格按其绘图,然后运行
d2-facts --expect

The loop

工作循环

1. write arch.facts.json                             the boxes, edges and width the brief asks for
2. write arch.d2                                     ids ASCII; the label after the colon is what the reader sees
3. d2 fmt --check arch.d2 && d2 validate arch.d2     syntax; exit 1 names file:line:col
4. node assets/d2-facts.mjs arch.d2 --expect arch.facts.json
                                                     duplicates, missing / reversed / invented edges,
                                                     container members, overlaps, order, width
5. d2 --layout=tala arch.d2 arch.png                 then READ arch.png with your file-reading tool — you can
                                                     look at an image, and this is the only step that shows
                                                     labels, shapes, key badges and which row an arrow leaves.
                                                     Both writers who skipped it reported the same thing:
                                                     "a PNG read would have answered this"
6. d2 --layout=tala arch.d2 arch.txt && cat arch.txt the deliverable, if it is going in a terminal
7. fix the text; go to 3
Five rounds at most. Each step answers a different question: 3 is "does it compile", 4 is "does it say what I meant", 5 is "does it read", 6 is "does it fit where it is going". A compile error is one line —
arch.d2:2:9: invalid style keyword: "colour"
,
2:1: connection missing destination
— fix the line it names.
1. write arch.facts.json                             the boxes, edges and width the brief asks for
2. write arch.d2                                     ids ASCII; the label after the colon is what the reader sees
3. d2 fmt --check arch.d2 && d2 validate arch.d2     syntax; exit 1 names file:line:col
4. node assets/d2-facts.mjs arch.d2 --expect arch.facts.json
                                                     duplicates, missing / reversed / invented edges,
                                                     container members, overlaps, order, width
5. d2 --layout=tala arch.d2 arch.png                 then READ arch.png with your file-reading tool — you can
                                                     look at an image, and this is the only step that shows
                                                     labels, shapes, key badges and which row an arrow leaves.
                                                     Both writers who skipped it reported the same thing:
                                                     "a PNG read would have answered this"
6. d2 --layout=tala arch.d2 arch.txt && cat arch.txt the deliverable, if it is going in a terminal
7. fix the text; go to 3
最多进行五轮。每一步回答不同的问题:第 3 步是“能否编译”,第 4 步是“是否表达了我的本意”,第 5 步是“是否易读”,第 6 步是“是否适合目标位置”。编译错误通常只有一行——
arch.d2:2:9: invalid style keyword: "colour"
、
2:1: connection missing destination
——修正它指出的那一行即可。

Done condition

完成条件

  • d2 validate
    exits 0 and
    d2 fmt --check
    exits 0.
  • d2 layout
    listed
    tala (bundled)
    and the render used it.
  • d2-facts --expect
    exits 0: no duplicate name, no truncated render, every listed box and edge drawn with its direction, no forbidden edge, the width inside budget — measured with the same
    --seeds
    the final render will use.
  • You read the PNG once, and it is one of the deliverables.
  • Every shape and connection in the prose is named by its id, and every id in the prose is in the file.
  • The
    .d2
    is committed where the SVG is; the SVG is a build output or is committed next to it — never only pasted.
  • d2 validate
    和
    d2 fmt --check
    均以 0 退出。
  • d2 layout
    的输出中列出了
    tala (bundled)
    ,且 render 确实使用了它。
  • d2-facts --expect
    以 0 退出:不存在重复名称或截断的 render;fact sheet 列出的每个 box 和 edge 都按正确方向绘制;不存在 forbidden edge;宽度未超出预算——测量时使用与最终 render 相同的
    --seeds
    。
  • 你查看过 PNG 一次,且 PNG 是交付物之一。
  • 正文中提到的每个 shape 和 connection 都以其 id 标识,且正文中的每个 id 都存在于文件中。
  • .d2
    与 SVG 提交在同一位置;SVG 要么是 build output,要么与其一同提交——绝不能只粘贴 SVG。

Width: measure it the way a terminal sees it

宽度:按照 terminal 的显示方式测量

bash
LC_ALL=C.UTF-8 wc -L arch.txt      # 122 — correct, and CJK-aware
wc -L arch.txt                     # 120 in the C locale: an undercount, and 87 vs 111 on a Japanese figure
awk '{if(length($0)>m)m=length($0)}END{print m}' arch.txt   # 338 — BYTES. Never this.
d2-facts.mjs
prints the same number as
columns
, which is the one to quote. Two writers hit the undercount independently; one spent a round shrinking a render that was already inside budget.
The levers are not monotone. Measure after every one. Measured on two different diagrams in the same round:
LeverDiagram ADiagram B
root
direction: right
→
down
144 → 145 (worse)148 → 109 (the win)
shorter labels144 → 146 (worse)109 → 135 (worse)
per-container
direction
on the fullest container
146 → 113 (the win)—
--tala-seeds 4,5,6
113 → 100no change across 12 seed sets
top
/
left
pin on one shape
113 → 74102 → 75
So: set the root
direction
, then the own
direction
of the container holding the most boxes
— that is the first thing to try, not the last — and re-measure after each. Shortening labels is as likely to make it wider, because TALA reflows the whole picture.
--scale
does not change the text render at all. None of this applies to a
sequence_diagram
, which lays itself out: there the only lever is label length, and it moves the width by a column or two in either direction.
Two caveats on the escape hatches:
  • Per-container
    direction
    is often ignored.
    Setting it on two containers produced byte-identical renders; on the fullest one it moved 33 columns. TALA appears to honour it only where the container's internal flow is ambiguous.
  • top
    /
    left
    are SVG pixels, not columns
    , and both must be set together (TALA only). Roughly 11.5px per terminal column on a 100-column figure, but every writer who used it found its numbers by trying three to five values. Pin the one or two shapes whose place matters, comment why, and expect the pin to need re-tuning after the next label change.
  • A pin can truncate the terminal render, and that looks like a win. One writer pinned a table, watched the width fall 113 → 71, and the number was a fragment: the pin had pushed shapes off the ascii canvas and three of five tables were gone from the text while the SVG had grown.
    d2
    exits 0 on it.
    d2-facts
    now reports it as an error; if you pin, re-read the render.
  • The budget may be unreachable without a pin. On five
    sql_table
    s no documented lever got under 100 columns:
    direction: down
    reached 113, shorter labels 127, a wrapping container 121, and only the unsafe pin went lower. When that happens, split the diagram by container into two files, or keep the wider figure and say in the prose that the terminal version is 113 columns — do not buy the number with a pin you have not re-read.
bash
LC_ALL=C.UTF-8 wc -L arch.txt      # 122 — correct, and CJK-aware
wc -L arch.txt                     # 120 in the C locale: an undercount, and 87 vs 111 on a Japanese figure
awk '{if(length($0)>m)m=length($0)}END{print m}' arch.txt   # 338 — BYTES. Never this.
d2-facts.mjs
会输出与
columns
相同的数值,这才是应引用的数字。两位编写者都独立遇到了少计问题;其中一人花了一轮去缩小一个本来已经符合预算的 render。
这些调节手段并非单调。每调整一次都要重新测量。 在同一轮中对两个不同 diagram 的实测结果如下:
调节手段Diagram ADiagram B
root 的
direction: right
→
down
144 → 145(更差)148 → 109(成功)
缩短 label144 → 146(更差)109 → 135(更差)
在最复杂的 container 上设置自身
direction
146 → 113(成功)—
--tala-seeds 4,5,6
113 → 10012 组 seed 均无变化
对一个 shape 使用
top
/
left
pin
113 → 74102 → 75
因此:先设置 root 的
direction
,然后设置包含 box 最多的 container 自身的
direction
——这应该是首先尝试的手段,而不是最后才尝试——并在每次调整后重新测量。缩短 label 反而同样可能让图变宽,因为 TALA 会重新排布整张图。
--scale
完全不会改变 text render。上述方法均不适用于
sequence_diagram
,因为它会自行布局;对它而言,唯一的调节手段是 label 长度,而长度只会让宽度增加或减少一两列。
使用这些非常规手段时要注意两点:
  • 每个 container 的
    direction
    经常会被忽略。
    在两个 container 上设置它,得到的 render 逐字节相同;设置在 box 最多的 container 上,则使宽度变化了 33 列。TALA 似乎只会在 container 内部流向不明确时才采用该设置。
  • top
    /
    left
    的单位是 SVG 像素,而不是列
    ,而且必须同时设置两者(仅限 TALA)。在 100 列的图中,每 terminal 列大约对应 11.5px,但每位使用它的编写者都是通过尝试三到五个值来找到合适数值。固定位置重要的一两个 shape,添加注释说明原因,并预期下次修改 label 后需要重新调整 pin。
  • Pin 可能截断 terminal render,而且表面上看起来像是优化成功。 一位编写者固定了一张 table,看到宽度从 113 降到 71,但这个数字只对应一段残片:pin 将 shape 推出了 ASCII canvas,五张 table 中有三张从 text 中消失,而 SVG 反而变大了。
    d2
    仍会返回 0。现在
    d2-facts
    会将其报告为错误;使用 pin 后必须重新检查 render。
  • 不使用 pin 时,宽度预算可能无法达到。 对五个
    sql_table
    ,所有有文档的调节手段都无法压到 100 列以内:
    direction: down
    降到 113,缩短 label 后为 127,使用 wrapping container 后为 121,只有不安全的 pin 能进一步降低。此时应按 container 将 diagram 拆成两个文件,或者保留较宽的图,并在正文中说明 terminal 版本为 113 列——不要用尚未重新检查过的 pin 来换取数字。

Reading the terminal render

阅读 terminal render

Measured on 0.8.1-HEAD. Read structure here; read labels and shapes on the PNG.
  • Edge labels are written onto the line and their spaces become line characters (
    vlmkit check integrity─page─html
    ,
    201─Created
    ). Keep terminal-bound edge labels to one or two words.
  • Every connection style is dropped.
    stroke-dash
    ,
    stroke
    ,
    bold
    render as a plain line: a writer diffed its styled render against a style-free copy and they were identical. Dashed-means-async does not survive, so carry the distinction in the label.
  • Arrowheads are unreliable on long labels. A label long enough to fill the lane can leave the arrow with no head at all, and
    <<
    in a label is eaten outright. Which character the line eats is not stable either: the same file lost a label's parentheses on one round and its full stop on the next (
    order.placed
    →
    order─placed
    ). Probe the spelling you land on, and prefer one short word with no punctuation.
  • shape: text
    and
    |md …|
    blocks render as empty boxes.
    A title, a legend or a paragraph belongs in the prose around the render, or in a variant of the file rendered only to SVG.
  • sql_table
    rows render, their constraint badges do not.
    PK / FK markers are visible only in the SVG / PNG, and a column-level edge attaches to the wrong row in the text view often enough that you cannot read it there.
  • An edge label can land on a box or container border (
    ┌────sync call────┐│
    ), which reads as two boxes merged. Where it lands is positional, not length-driven: a writer whose connection was already at the root both shortened and lengthened the label and it stayed on the same border row at the same width. The levers that do move it are the ones that reflow the picture — the container's
    direction
    , a different seed — so treat it as a layout outcome to check on the PNG, not a label to trim.
  • CJK labels pad every glyph (
    ウ  ェ  ブ
    ) and misalign the box that holds them. Ids stay ASCII always; for a figure that will be read in a terminal keep the labels ASCII too and put the Japanese in the surrounding text. The SVG and PNG render Japanese correctly.
  • standard
    mode
    keeps the same layout with
    + - | / < > v
    only — the version for a README read on GitHub, where box-drawing glyphs in a code block depend on the viewer's font.
  • Icons and images are dropped; sequence diagrams,
    sql_table
    and
    class
    shapes render, with their rows.
以下结果基于 0.8.1-HEAD 实测。这里用于阅读结构;label 和 shape 应在 PNG 上阅读。
  • Edge label 会直接写在连线上,其中的空格会变成线条字符(
    vlmkit check integrity─page─html
    、
    201─Created
    )。面向 terminal 的 edge label 应控制在一到两个词。
  • 所有 connection style 都会被丢弃。
    stroke-dash
    、
    stroke
    、
    bold
    均会 render 为普通直线:一位编写者对带 style 的 render 与无 style 副本做 diff,结果完全相同。虚线表示 async 这一信息无法保留,因此应将区别写入 label。
  • Label 较长时,arrowhead 不可靠。 足以填满 lane 的 label 可能让 arrow 完全没有头部,而 label 中的
    <<
    也会被直接吞掉。线条吞掉哪个字符并不稳定:同一文件有一轮丢失 label 的括号,下一轮又丢失句号(
    order.placed
    →
    order─placed
    )。应检查最终呈现的拼写,并优先使用一个不含标点的短词。
  • shape: text
    和
    |md …|
    block 会 render 为空 box。
    标题、legend 或段落应放在 render 周围的正文中,或者放进一个仅 render 为 SVG 的文件变体。
  • sql_table
    的 row 会 render,但 constraint badge 不会。
    PK / FK 标记仅在 SVG / PNG 中可见;而且 column 级 edge 在 text view 中经常连接到错误的 row,无法据此读取实际关系。
  • Edge label 可能落在 box 或 container 边框上(
    ┌────sync call────┐│
    ),看起来就像两个 box 合并了。其落点由位置决定,而不是由长度决定:一位编写者的 connection 已写在 root,他既缩短又加长了 label,但它仍停留在同一宽度下的同一行边框上。真正能移动它的手段是那些会重新排布整张图的方式——container 的
    direction
    、不同的 seed——因此应把它视为需要在 PNG 上检查的布局结果,而不是靠裁剪 label 来解决。
  • CJK label 会为每个 glyph 补齐宽度(
    ウ  ェ  ブ
    ),导致容纳它们的 box 错位。Id 必须始终使用 ASCII;对于将在 terminal 中阅读的图,label 也应保持 ASCII,把日文放在周围的正文中。SVG 和 PNG 能正确 render 日文。
  • standard
    mode
    保持相同布局,但只使用
    + - | / < > v
    ——这是面向在 GitHub 上阅读 README 的版本,因为 code block 中的线框字符效果取决于阅读者所用字体。
  • Icon 和 image 会被丢弃;sequence diagram、
    sql_table
    与
    class
    shape 则会连同其 row 一起 render。

Rules the writer keeps

编写者应遵守的规则

  • Ids are ASCII and stable (
    api
    , not
    API server
    ); the label goes after the colon. Abbreviating an id is fine —
    d2-facts
    matches on the label too — but the next editor reads the id first.
  • A cross-container connection is written at the root or with
    _.
    , and
    _
    is one level: from inside
    cluster
    , the root is
    _
    , so a shape in a sibling container is
    _.outside.stripe
    . Never let an out-of-scope name stand: it becomes a new box (see above).
  • One root
    direction
    ; a container that must flow the other way sets its own (TALA only, and only sometimes honoured).
  • Connections read as sentences:
    a -> b: verb
    .
    --
    is undirected,
    <->
    both ways. A style on a connection (
    {style.stroke-dash: 3}
    ) is invisible in the terminal — if the distinction matters there, put it in the label.
  • vars: { d2-config: { layout-engine: tala } }
    at the top of every file, so a render without the flag still gets TALA for the SVG.
  • No coordinates unless a shape must be pinned; then
    top
    and
    left
    together, on that shape only, and a comment saying why.
  • Id 应使用 ASCII 且保持稳定(用
    api
    ,而不是
    API server
    );label 写在冒号后。可以缩写 id——
    d2-facts
    也会匹配 label——但下一位编辑者会先阅读 id。
  • 跨 container 的 connection 应写在 root,或使用
    _.
    ,且
    _
    只表示一级:在
    cluster
    内部,root 是
    _
    ,因此 sibling container 中的 shape 写作
    _.outside.stripe
    。绝不能保留超出作用域的名称:它会变成新的 box(见上文)。
  • Root 只设置一个
    direction
    ;需要采用相反流向的 container 设置自身
    direction
    (仅 TALA 支持,而且并非总会采用)。
  • Connection 应像句子一样阅读:
    a -> b: verb
    。
    --
    表示无方向,
    <->
    表示双向。Connection 上的 style(
    {style.stroke-dash: 3}
    )在 terminal 中不可见——如果该区别在那里很重要,请写入 label。
  • 每个文件顶部都写上
    vars: { d2-config: { layout-engine: tala } }
    ,这样即使 render 时未传 flag,SVG 仍会使用 TALA。
  • 除非必须固定某个 shape 的位置,否则不要使用坐标;需要固定时,只在该 shape 上同时设置
    top
    和
    left
    ,并添加注释说明原因。

Deliver

交付

  • In a chat: the terminal render in a fenced block (it is the picture in a terminal), the ids named in the prose, the
    .d2
    and the PNG / SVG as files (
    SendUserFile
    when available).
  • In a PR or issue: the
    standard
    render in a fenced block, or the SVG committed and linked. The
    .d2
    in the diff is the review target.
  • In docs: the
    .d2
    committed next to the page and the SVG generated by the build (
    d2 --layout=tala docs/arch.d2 docs/arch.svg
    in a script), or the SVG committed when the site has no build step.
  • 在聊天中:以 fenced block 展示 terminal render(它就是 terminal 中的图片),在正文中点名 id,并将
    .d2
    与 PNG / SVG 作为文件提供(可用时使用
    SendUserFile
    )。
  • 在 PR 或 issue 中:在 fenced block 中展示
    standard
    render,或者提交并链接 SVG。diff 中的
    .d2
    才是评审对象。
  • 在文档中:把
    .d2
    提交到页面旁边,并由 build 生成 SVG(在 script 中执行
    d2 --layout=tala docs/arch.d2 docs/arch.svg
    );如果站点没有 build step,则直接提交 SVG。

Failure modes

故障模式

  • d2: command not found
    → install (above); do not draw it in another tool.
  • d2 layout
    does not list
    tala
    → an old release; upgrade. The installer's
    --tala
    flag no longer exists and is rejected.
  • A box appears twice, or an arrow you wrote is nowhere → an out-of-scope reference created a phantom.
    d2-facts
    names it; the fix is a full path or
    _.
    , not another arrow.
  • The text render came out dagre-shaped and wide though the file says
    layout-engine: tala
    → the
    .txt
    path ignores the in-file config on some builds; pass
    --layout=tala
    .
  • "tala-seeds" needs a value
    /
    is not a valid config
    → seeds are a flag only.
  • connection missing destination
    ,
    maps must be terminated with }
    → the line and column are named; usually a trailing
    ->
    or an unclosed brace.
  • invalid style keyword
    → D2 spells
    color
    ,
    stroke
    ,
    stroke-dash
    ,
    fill
    ,
    font-size
    ,
    bold
    ,
    opacity
    ,
    shadow
    ,
    3d
    ,
    multiple
    .
  • Every round moves the boxes though you changed one label → TALA reflowed. Pin the few shapes whose place matters, or accept it and stop describing positions.
  • Two boxes look like they touch and you cannot tell →
    d2-facts
    reports sibling overlaps in px²; D2 itself has no collision report.
assets/vlmkit-workspace.d2
is vlmkit's workspace (mizchi/vlmkit) as a D2 file, and
assets/vlmkit-workspace.txt
is its TALA terminal render — what a clean round looks like. The validation round that produced the numbers above is
tests/d2/fixtures/d2-scenario/
and
docs/reports/2026-09-14-d2-diagram-v1.md
.
  • d2: command not found
    → 按上文方式安装;不要改用其他工具绘图。
  • d2 layout
    未列出
    tala
    → 版本过旧;请升级。安装器的
    --tala
    flag 已不存在,使用时会被拒绝。
  • 某个 box 出现两次,或你写的 arrow 完全不见 → 超出作用域的引用创建了幽灵节点。
    d2-facts
    会指出它;修复方式是使用完整路径或
    _.
    ,而不是再添加一条 arrow。
  • 尽管文件写着
    layout-engine: tala
    ,text render 却呈现 dagre 风格且很宽 → 某些构建在处理
    .txt
    时会忽略文件内配置;请传入
    --layout=tala
    。
  • "tala-seeds" needs a value
    /
    is not a valid config
    → seed 只能通过 flag 设置。
  • connection missing destination
    、
    maps must be terminated with }
    → 错误会指出具体行和列;通常是末尾多了
    ->
    或花括号未闭合。
  • invalid style keyword
    → D2 中的正确拼写是
    color
    、
    stroke
    、
    stroke-dash
    、
    fill
    、
    font-size
    、
    bold
    、
    opacity
    、
    shadow
    、
    3d
    、
    multiple
    。
  • 只改了一个 label,却每轮都有 box 移动 → TALA 重新布局了。可固定位置重要的少数 shape,或接受变化并停止描述位置。
  • 两个 box 看起来相接却无法判断 →
    d2-facts
    会以 px² 报告 sibling 重叠;D2 本身没有 collision report。
assets/vlmkit-workspace.d2
是 vlmkit workspace(mizchi/vlmkit)的 D2 文件,
assets/vlmkit-workspace.txt
则是其 TALA terminal render——这就是一轮干净结果的样子。产生上述数字的验证轮次位于
tests/d2/fixtures/d2-scenario/
,报告见
docs/reports/2026-09-14-d2-diagram-v1.md
。