diff --git a/README.ZH.md b/README.ZH.md
index fcc8ceb..da8b12e 100644
--- a/README.ZH.md
+++ b/README.ZH.md
@@ -13,11 +13,27 @@
引擎能画的所有控件——滑块、带刻度的范围滑块、分段控件、单选组、开关、
自定义取色器、条件字段——全部由 schema 提示驱动,skill 也会明确要求 agent 主动
-用起来。完整清单见
-[`schemaui` 控件画廊](https://github.com/YuniqueUnic/schemaui/blob/main/examples/controls-gallery.schema.json);
+用起来。图形走同一套提示:问题和选项都可以带 **Mermaid 图、内联 SVG 或 Markdown
+说明**(`x-content` / `x-options`),答案本身也可以是一张可编辑的
+实时预览图(`x-control: "mermaid"`)。完整清单见
+[`schemaui` 控件画廊](https://github.com/YuniqueUnic/schemaui/blob/main/examples/controls-gallery.schema.json)
+与
+[富内容画廊](https://github.com/YuniqueUnic/schemaui/blob/main/examples/rich-content.schema.json);
[`examples/web-research-brief.schema.json`](./examples/web-research-brief.schema.json)
是把这些控件全部用上的一份中文范例。
+
+
## 为什么需要它
Agent 的工具调用跑在**没有 TTY** 的子进程里——终端提示符根本渲染不出 来,agent
@@ -130,20 +146,28 @@ Windows / PowerShell 7+:`pwsh scripts/ask.ps1 -Schema … -Title …`。没有 P
- **给每个答案配对的控件** —— 滑块、带刻度的滑块、双柄范围、取色器、分段控件、
单选组、复选框、条件字段,外加文本、数值、单选/多选、oneOf 组合、嵌套对
象、记录列表、键值映射。
+- **能看图就别读字** —— 选项差在「形状」上时(拓扑、发布策略、数据模型), agent
+ 给每个选项配一张图,你认图选择;节点级 `x-content` 用图或一段富文本
+ 解释字段背景;`mermaid` 控件让答案本身就是一张可编辑、实时预览的图。
+- **访谈纪律内置** —— 开放式设计需要深度访谈时,skill 优先使用你已安装的
+ grilling 类 skill,都没有时回退到内置的
+ [`skills/grill-with-docs`](./skills/grill-with-docs/SKILL.md):一次一问、
+ 每问带推荐答案、每个问题都以项目文档为依据。
## 示例
[`examples/`](./examples/) 里可直接运行的表单 (每个都带 `.defaults.json`
推荐答案):
-| 示例 | 场景 |
-| ----------------------------------------------------------------------------------- | --------------------------------------------------- |
-| [`env-schema.json`](./examples/env-schema.json) | 最小 4 字段部署表单 —— 首次冒烟测试 |
-| [`web-research-brief.schema.json`](./examples/web-research-brief.schema.json) | 调研:联网调研任务确认 —— 用上画廊全部控件 (中文) |
-| [`feature-brief.schema.json`](./examples/feature-brief.schema.json) | 12 问需求简报,覆盖全部控件类型 (英文) |
-| [`invoice-reimbursement.schema.json`](./examples/invoice-reimbursement.schema.json) | 办公:发票报销处理 (中文,每个选择带「其他」逃生口) |
-| [`ecommerce-main-image.schema.json`](./examples/ecommerce-main-image.schema.json) | 设计:电商主图 —— 尺寸、字号、颜色、渐变背景 (中文) |
-| [`seo-diagnosis.schema.json`](./examples/seo-diagnosis.schema.json) | SEO 排查:站点、问题、关键词、竞品 (中文) |
+| 示例 | 场景 |
+| --------------------------------------------------------------------------------------- | --------------------------------------------------- |
+| [`env-schema.json`](./examples/env-schema.json) | 最小 4 字段部署表单 —— 首次冒烟测试 |
+| [`web-research-brief.schema.json`](./examples/web-research-brief.schema.json) | 调研:联网调研任务确认 —— 用上画廊全部控件 (中文) |
+| [`feature-brief.schema.json`](./examples/feature-brief.schema.json) | 12 问需求简报,覆盖全部控件类型 (英文) |
+| [`invoice-reimbursement.schema.json`](./examples/invoice-reimbursement.schema.json) | 办公:发票报销处理 (中文,每个选择带「其他」逃生口) |
+| [`ecommerce-main-image.schema.json`](./examples/ecommerce-main-image.schema.json) | 设计:电商主图 —— 尺寸、字号、颜色、渐变背景 (中文) |
+| [`seo-diagnosis.schema.json`](./examples/seo-diagnosis.schema.json) | SEO 排查:站点、问题、关键词、竞品 (中文) |
+| [`deployment-architecture.schema.json`](./examples/deployment-architecture.schema.json) | 架构选型:每个选项自带一张图 (中文,图形界面参考例) |
## 脚本契约
diff --git a/README.md b/README.md
index ea973e2..c927935 100644
--- a/README.md
+++ b/README.md
@@ -16,11 +16,28 @@ you and the agent can re-read later.
Every control the engine can draw — sliders, range sliders with marks, segmented
controls, radio groups, switches, a custom color picker, conditional fields —
driven entirely by schema hints, and the skill tells the agent to reach for
-them. The full set lives in
-[`schemaui`'s controls gallery](https://github.com/YuniqueUnic/schemaui/blob/main/examples/controls-gallery.schema.json);
+them. Figures ride the same hints: a question or an option can carry a **Mermaid
+diagram, an inline SVG, or sanitised Markdown** (`x-content` / `x-options`), and
+an answer can _be_ a diagram via the live-preview `x-control: "mermaid"` editor.
+The full set lives in
+[`schemaui`'s controls gallery](https://github.com/YuniqueUnic/schemaui/blob/main/examples/controls-gallery.schema.json)
+and
+[rich-content gallery](https://github.com/YuniqueUnic/schemaui/blob/main/examples/rich-content.schema.json);
[`examples/web-research-brief.schema.json`](./examples/web-research-brief.schema.json)
is the worked example that uses every one of them (中文).
+
+
## Why
Agent tool calls run in subprocesses with **no TTY** — a terminal prompt renders
@@ -138,20 +155,31 @@ without Python: `bash scripts/ask.sh --schema …`.
ranges, colour pickers, segmented controls, radio groups, checkboxes, and
conditional fields, alongside text, number, single/multi select, oneOf
compositions, nested objects, record lists, and key/value maps.
+- **Show, don't tell** — when options differ in _shape_ (topologies, rollout
+ strategies, data models), the agent attaches a diagram to each option and lets
+ you pick by recognising the picture; node-level `x-content` explains a field
+ with a figure or prose; a `mermaid` control makes the answer itself an
+ editable, live-previewed diagram.
+- **Interview discipline built in** — for open-ended designs, the skill defers
+ to whatever grilling skill you already have, and falls back to the bundled
+ [`skills/grill-with-docs`](./skills/grill-with-docs/SKILL.md): one question at
+ a time, every question with a recommended answer, every question grounded in
+ the project's docs.
## Examples
Runnable forms in [`examples/`](./examples/) (each with recommended answers in
the matching `.defaults.json`):
-| Example | Scenario |
-| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
-| [`env-schema.json`](./examples/env-schema.json) | minimal 4-field deploy form — first smoke test |
-| [`web-research-brief.schema.json`](./examples/web-research-brief.schema.json) | research: scope a web-research task — every control in the gallery (中文) |
-| [`feature-brief.schema.json`](./examples/feature-brief.schema.json) | 12-question requirements brief using every control type (EN) |
-| [`invoice-reimbursement.schema.json`](./examples/invoice-reimbursement.schema.json) | office: invoice & expense reimbursement (中文) |
-| [`ecommerce-main-image.schema.json`](./examples/ecommerce-main-image.schema.json) | design: e-commerce hero images — sizes, fonts, colors, backgrounds (中文) |
-| [`seo-diagnosis.schema.json`](./examples/seo-diagnosis.schema.json) | SEO triage: site, issues, keywords, competitors (中文) |
+| Example | Scenario |
+| --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
+| [`env-schema.json`](./examples/env-schema.json) | minimal 4-field deploy form — first smoke test |
+| [`web-research-brief.schema.json`](./examples/web-research-brief.schema.json) | research: scope a web-research task — every control in the gallery (中文) |
+| [`feature-brief.schema.json`](./examples/feature-brief.schema.json) | 12-question requirements brief using every control type (EN) |
+| [`invoice-reimbursement.schema.json`](./examples/invoice-reimbursement.schema.json) | office: invoice & expense reimbursement (中文) |
+| [`ecommerce-main-image.schema.json`](./examples/ecommerce-main-image.schema.json) | design: e-commerce hero images — sizes, fonts, colors, backgrounds (中文) |
+| [`seo-diagnosis.schema.json`](./examples/seo-diagnosis.schema.json) | SEO triage: site, issues, keywords, competitors (中文) |
+| [`deployment-architecture.schema.json`](./examples/deployment-architecture.schema.json) | architecture choice: every option carries a diagram (中文 — the figures reference) |
## Script contract
diff --git a/SKILL.md b/SKILL.md
index 6e4e1a9..72e60d2 100644
--- a/SKILL.md
+++ b/SKILL.md
@@ -4,10 +4,12 @@ description: >
When the user must choose among options or fill in structured config, spawn
a browser form (powered by schemaui Web UI, bound to 0.0.0.0), open the
user's browser, and write the result to .schemaui/answers/. Never use a
- terminal/TUI prompt — the agent process has no TTY attachment. Trigger
- phrases: "give me a form", "ask me via form", "configure", "pick a deploy
- environment", "I need to choose", "a2ui", "给我个表单", "配置一下",
- "用表单问我", "问我几个问题".
+ terminal/TUI prompt — the agent process has no TTY attachment. Supports
+ rich content: figures on questions and answer options (Mermaid/SVG/Markdown
+ via x-content / x-options) and a live Mermaid editor (x-control: mermaid).
+ Trigger phrases: "give me a form", "ask me via form", "configure", "pick a
+ deploy environment", "I need to choose", "grill me", "a2ui", "给我个表单",
+ "配置一下", "用表单问我", "问我几个问题", "拷问我".
---
# a2ui-ask — collect user input via a browser form (file output)
@@ -109,29 +111,48 @@ channel list: see `install.md` in this repository.
the field forever. The controlling field's `default` must not be the escape
value, or the input would be visible from the start.
+## Grilling: which interview skill to use
+
+Open-ended designs sometimes need a relentless interview before they become a
+form. When the user asks to be grilled (or a plan needs stress-testing before
+you can draft the schema), use an existing grilling skill if the user named or
+installed one — check their skills directory for something like `grill-me`,
+`grilling`, or a skill they explicitly referenced, and defer to it. Only when
+none exists, fall back to the one bundled here:
+[`skills/grill-with-docs/SKILL.md`](./skills/grill-with-docs/SKILL.md) — the
+same one-question-at-a-time, recommend-then-ask discipline, plus a rule to
+ground every question in the project's docs and the galleries referenced above,
+and to end by turning the settled decisions into a form. The bundled skill is a
+convenience so a fresh install works out of the box; it never overrides a
+grilling skill the user chose themselves.
+
## Question type → schema cheat sheet
-| You need | Schema shape | Renders as |
-| --------------------- | -------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
-| short text input | `{"type": "string", "minLength": …, "pattern": …}` | inline text field |
-| long text input | `{"type": "string", "x-multiline": true}` | multi-line text area |
-| number | `{"type": "integer", "minimum": …, "maximum": …}` | numeric field with guards |
-| number on a scale | `{"type": "…", "minimum": …, "maximum": …, "x-control": "slider"}` | slider with a live readout |
-| slider with labels | slider + `"x-slider-marks": [{"value": …, "label": …}, …]` | slider with labelled stops |
-| two-number interval | `{"type": "array", "items": {"type": "number"}, "minItems": 2, "maxItems": 2, "minimum": …, "maximum": …, "x-control": "range"}` | two-handle range slider |
-| colour | `{"type": "string", "x-control": "color"}` | colour picker, not a text box |
-| few short options | `{"type": "string", "enum": [...], "x-control": "segmented"}` | inline segmented control |
-| few verbose options | `{"type": "string", "enum": [...], "x-control": "radio"}` | stacked radio group |
-| yes/no | `{"type": "boolean"}` | toggle |
-| yes/no, part of a set | `{"type": "boolean", "x-control": "checkbox"}` | checkbox |
-| single select | `{"type": "string", "enum": [...]}` | popup selector |
-| multi select | `{"type": "array", "items": {"enum": [...]}, "uniqueItems": true}` | checkbox list (one per option) |
-| select + escape hatch | `enum: [..., "其他"]` plus a sibling `"_custom"` carrying `x-visible-when` | selector, then a text field once 「其他」 is picked |
-| conditional field | `"x-visible-when": {"field": , "op": "equals"\|"contains", "value": …}` | hidden until the sibling matches |
-| pick-one-with-config | `{"oneOf": [{"title": "A", …}, {"title": "B", …}]}` | variant chooser + subform |
-| grouped fields | `{"type": "object", "properties": {…}}` | nested section |
-| list of records | `{"type": "array", "items": {"type": "object", "properties": {…}}}` | list + per-entry overlay |
-| free-form key/value | `{"type": "object", "additionalProperties": {"type": "string"}}` | key/value editor |
+| You need | Schema shape | Renders as |
+| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
+| short text input | `{"type": "string", "minLength": …, "pattern": …}` | inline text field |
+| long text input | `{"type": "string", "x-multiline": true}` | multi-line text area |
+| number | `{"type": "integer", "minimum": …, "maximum": …}` | numeric field with guards |
+| number on a scale | `{"type": "…", "minimum": …, "maximum": …, "x-control": "slider"}` | slider with a live readout |
+| slider with labels | slider + `"x-slider-marks": [{"value": …, "label": …}, …]` | slider with labelled stops |
+| two-number interval | `{"type": "array", "items": {"type": "number"}, "minItems": 2, "maxItems": 2, "minimum": …, "maximum": …, "x-control": "range"}` | two-handle range slider |
+| colour | `{"type": "string", "x-control": "color"}` | colour picker, not a text box |
+| few short options | `{"type": "string", "enum": [...], "x-control": "segmented"}` | inline segmented control |
+| few verbose options | `{"type": "string", "enum": [...], "x-control": "radio"}` | stacked radio group |
+| yes/no | `{"type": "boolean"}` | toggle |
+| yes/no, part of a set | `{"type": "boolean", "x-control": "checkbox"}` | checkbox |
+| single select | `{"type": "string", "enum": [...]}` | popup selector |
+| multi select | `{"type": "array", "items": {"enum": [...]}, "uniqueItems": true}` | checkbox list (one per option) |
+| select + escape hatch | `enum: [..., "其他"]` plus a sibling `"_custom"` carrying `x-visible-when` | selector, then a text field once 「其他」 is picked |
+| conditional field | `"x-visible-when": {"field": , "op": "equals"\|"contains", "value": …}` | hidden until the sibling matches |
+| explain with a diagram | `"x-content": {"type": "mermaid", "source": "flowchart …"}` on the field | rendered figure above the control |
+| explain with prose | `"x-content": {"type": "markdown", "source": "**why** this matters…"}` | sanitised rich text above the control |
+| options worth seeing | `"x-options": [{"label": …, "description": …, "content": {"type": "mermaid", "source": …}}, …]` (aligned with `enum` by index) | each option shows its own figure |
+| edit a diagram | `{"type": "string", "x-control": "mermaid"}` | mermaid source editor with a live preview |
+| pick-one-with-config | `{"oneOf": [{"title": "A", …}, {"title": "B", …}]}` | variant chooser + subform |
+| grouped fields | `{"type": "object", "properties": {…}}` | nested section |
+| list of records | `{"type": "array", "items": {"type": "object", "properties": {…}}}` | list + per-entry overlay |
+| free-form key/value | `{"type": "object", "additionalProperties": {"type": "string"}}` | key/value editor |
`x-visible-when` is not only for escape hatches: use it whenever a question only
applies conditionally (`has_changes` → `change_notes`, `cache.enabled` →
@@ -162,18 +183,57 @@ removes typing or removes a wrong answer, not for decoration.
`x-control` is a _request_, and bounds stay validation keywords: a slider needs
`minimum`/`maximum` on the value, and a hint the engine cannot honour falls back
to that shape's default control rather than failing. Slider/range/colour/
-segmented/radio hints need `schemaui` ≥ 0.14 / `schemaui-cli` ≥ 0.8; an older
-engine ignores unknown `x-` keywords, so the form still runs — it just shows
-plain inputs everywhere.
+segmented/radio hints need `schemaui` ≥ 0.14 / `schemaui-cli` ≥ 0.8; the figure
+surfaces below need `schemaui` ≥ 0.16 / `schemaui-cli` ≥ 0.9. An older engine
+ignores unknown `x-` keywords, so the form still runs — it just shows plain
+inputs everywhere.
Read these before writing your own: `examples/web-research-brief.schema.json`
(中文 — the worked reference: every control in the gallery, escape hatches,
-conditional fields, `oneOf`, a record list, a key/value map),
-`examples/feature-brief.schema.json` (English, every control type) and
-`examples/invoice-reimbursement.schema.json` (中文, escape hatches on every
-select). The engine's own gallery,
-[`schemaui/examples/controls-gallery.schema.json`](https://github.com/YuniqueUnic/schemaui/blob/main/examples/controls-gallery.schema.json),
-shows every hint value side by side.
+conditional fields, `oneOf`, a record list, a key/value map, and figures on
+nodes and options), `examples/feature-brief.schema.json` (English, every control
+type) and `examples/invoice-reimbursement.schema.json` (中文, escape hatches on
+every select). For what figure-bearing options look like as a complete form,
+read `examples/deployment-architecture.schema.json` (中文 — every option carries
+a diagram). The engine's own galleries,
+[`schemaui/examples/controls-gallery.schema.json`](https://github.com/YuniqueUnic/schemaui/blob/main/examples/controls-gallery.schema.json)
+and
+[`schemaui/examples/rich-content.schema.json`](https://github.com/YuniqueUnic/schemaui/blob/main/examples/rich-content.schema.json),
+show every hint value and every figure surface side by side.
+
+## Figures: when a picture answers faster than prose
+
+Some choices are easier to **see** than to read. The engine renders three kinds
+of figure content — Mermaid diagrams, raw SVG, and sanitised Markdown — and puts
+them in three places. Reach for them when the user would otherwise have to
+mentally simulate your prose:
+
+1. **On the node** —
+ `"x-content": {"type": "mermaid"|"svg"|"markdown", "source": …}` renders a
+ figure above the control. Use it when the field needs context a sentence
+ can't carry: the request flow the timeout applies to, the pipeline the stage
+ names refer to, a legend of the states.
+2. **On the options** — `"x-options"` entries (aligned with `enum` by index) may
+ each carry `label`, `description`, and `content` (same shape as `x-content`).
+ Use it when the options are _shapes of a system_ — topologies, rollouts, data
+ models — and the user picks by recognising the picture. A select shows each
+ option's figure inline; `radio` shows it under the label with the
+ description.
+3. **As the value itself** — `"x-control": "mermaid"` makes the field's string
+ value a Mermaid source, edited in a live-preview editor. Only for fields
+ whose _answer is a diagram_ (a flow to review, a topology to correct) — never
+ as decoration on a config answer.
+
+Hard rules (the engine rejects the schema on these): every `source` must be a
+non-empty string; an `x-options` array aligns with `enum` by index, so keep the
+lengths aligned and give plain options `{}`/a `label` only; **`segmented` cannot
+show figures** — use `radio` or `select` when options carry content; options
+that mean "none/not configured" should not carry a figure. A figure is
+explanatory, never the value: the answer file still receives the enum string.
+
+Choose figures the way you choose controls — because they remove work, not for
+decoration. A three-option enum with one-word labels needs no diagrams; an
+architecture choice whose options differ in shape is exactly what they're for.
## Steps
@@ -283,14 +343,15 @@ something similar. Reuse a prior answer as defaults via `--config`.
Six runnable forms live in `examples/` (each with a `.defaults.json` carrying
the recommended answers):
-| Example | Scenario |
-| ----------------------------------- | ------------------------------------------------------------------------------------ |
-| `env-schema.json` | minimal 4-field deploy form — first smoke test |
-| `web-research-brief.schema.json` | research: scope a web-research task — **every control in the gallery** (中文) |
-| `feature-brief.schema.json` | 12-question requirements brief, every control type + both hints (EN) |
-| `invoice-reimbursement.schema.json` | office: invoice & expense reimbursement (中文, escape hatches) |
-| `ecommerce-main-image.schema.json` | design: e-commerce hero image specs — sizes, fonts, colors, oneOf backgrounds (中文) |
-| `seo-diagnosis.schema.json` | SEO triage: site, issues, keywords, competitors (中文) |
+| Example | Scenario |
+| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
+| `env-schema.json` | minimal 4-field deploy form — first smoke test |
+| `web-research-brief.schema.json` | research: scope a web-research task — **every control in the gallery** + figures on nodes and a mermaid editor (中文) |
+| `feature-brief.schema.json` | 12-question requirements brief, every control type + both hints (EN) |
+| `invoice-reimbursement.schema.json` | office: invoice & expense reimbursement (中文, escape hatches) |
+| `ecommerce-main-image.schema.json` | design: e-commerce hero image specs — sizes, fonts, colors, oneOf backgrounds (中文) |
+| `seo-diagnosis.schema.json` | SEO triage: site, issues, keywords, competitors (中文) |
+| `deployment-architecture.schema.json` | architecture choice: **every option carries a diagram** (中文 — the figure surfaces reference) |
```bash
python3 scripts/ask.py \
diff --git a/docs/controls-gallery.png b/docs/controls-gallery.png
new file mode 100644
index 0000000..05bfb86
Binary files /dev/null and b/docs/controls-gallery.png differ
diff --git a/docs/mermaid-editor.png b/docs/mermaid-editor.png
new file mode 100644
index 0000000..561134a
Binary files /dev/null and b/docs/mermaid-editor.png differ
diff --git a/docs/rich-options.png b/docs/rich-options.png
new file mode 100644
index 0000000..f717498
Binary files /dev/null and b/docs/rich-options.png differ
diff --git a/examples/deployment-architecture.defaults.json b/examples/deployment-architecture.defaults.json
new file mode 100644
index 0000000..924b0e8
--- /dev/null
+++ b/examples/deployment-architecture.defaults.json
@@ -0,0 +1,8 @@
+{
+ "topology": "monolith",
+ "rollout": "rolling",
+ "data_layer": "shared",
+ "network": {
+ "exposure": "公网负载均衡"
+ }
+}
diff --git a/examples/deployment-architecture.schema.json b/examples/deployment-architecture.schema.json
new file mode 100644
index 0000000..eb73a28
--- /dev/null
+++ b/examples/deployment-architecture.schema.json
@@ -0,0 +1,188 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "部署架构选择",
+ "description": "示例:选项差在「形状」上时,让每个选项自带一张图,用户看图选型而不是读文字。节点上的 x-content 先给出全局结构,选项的 x-options 图回答“这个选项长什么样”。",
+ "type": "object",
+ "required": [
+ "topology",
+ "rollout",
+ "data_layer"
+ ],
+ "properties": {
+ "topology": {
+ "type": "string",
+ "title": "服务拓扑",
+ "description": "radio 而不是 segmented:每个选项的图需要一整行的空间。图回答“长什么样”,description 回答“代价是什么”。",
+ "enum": [
+ "monolith",
+ "microservices",
+ "serverless"
+ ],
+ "default": "monolith",
+ "x-control": "radio",
+ "x-options": [
+ {
+ "label": "单体",
+ "description": "一个进程包办一切:部署与调试最简单,团队小时迭代最快。",
+ "content": {
+ "type": "mermaid",
+ "source": "flowchart TD\n ui[UI] --> app[App]\n app --> db[(DB)]"
+ }
+ },
+ {
+ "label": "微服务",
+ "description": "按业务边界拆分,独立部署与扩容;换来的是网关、网络调用与运维成本。",
+ "content": {
+ "type": "mermaid",
+ "source": "flowchart TD\n gw[gateway] --> a & b\n a --> dba[(db)]\n b --> dbb[(db)]"
+ }
+ },
+ {
+ "label": "Serverless",
+ "description": "函数 + 托管队列,按调用计费、自动扩缩;冷启动与厂商绑定是代价。",
+ "content": {
+ "type": "mermaid",
+ "source": "flowchart LR\n fn1(lambda) --> queue --> fn2(lambda)"
+ }
+ }
+ ]
+ },
+ "rollout": {
+ "type": "string",
+ "title": "发布策略",
+ "description": "同一套服务,三种把新版本送到用户手里的形状。金丝雀需要流量探测与回滚自动化,团队小时不建议首选。",
+ "enum": [
+ "rolling",
+ "blue-green",
+ "canary"
+ ],
+ "default": "rolling",
+ "x-control": "radio",
+ "x-options": [
+ {
+ "label": "滚动发布",
+ "description": "实例逐批替换,简单且资源占用最低。",
+ "content": {
+ "type": "mermaid",
+ "source": "flowchart LR\n v1[v1 v1 v1] --> v2[v1 v2 v2] --> v3[v2 v2 v2]"
+ }
+ },
+ {
+ "label": "蓝绿发布",
+ "description": "两套环境整组切换,回滚一瞬间;要付双倍资源的钱。",
+ "content": {
+ "type": "mermaid",
+ "source": "flowchart LR\n user(user) --> gw{switch}\n gw -->|now| blue[blue v1]\n gw -.->|after switch| green[green v2]"
+ }
+ },
+ {
+ "label": "金丝雀发布",
+ "description": "一小撮真实流量先试新版本,指标正常再放量。",
+ "content": {
+ "type": "mermaid",
+ "source": "flowchart LR\n user(user) --> lb{按比例}\n lb -->|95%| stable[v1 稳定版]\n lb -->|5%| canary[v2 金丝雀]"
+ }
+ }
+ ]
+ },
+ "data_layer": {
+ "type": "string",
+ "title": "数据层",
+ "description": "select 同样能带图:下拉里每个选项是一张小图,选中后大图留在控件下方。前两个选项各带图,转义选项「其他」不带——它没有形状可画。",
+ "enum": [
+ "shared",
+ "dedicated",
+ "其他"
+ ],
+ "default": "shared",
+ "x-options": [
+ {
+ "label": "共享数据库",
+ "description": "所有服务读写同一个库:事务简单,边界容易糊掉。",
+ "content": {
+ "type": "mermaid",
+ "source": "flowchart LR\n s1[服务 A] & s2[服务 B] --> db[(共享 DB)]"
+ }
+ },
+ {
+ "label": "每服务独享",
+ "description": "库随服务私有,边界清晰;跨服务查询要走 API。",
+ "content": {
+ "type": "mermaid",
+ "source": "flowchart LR\n s1[服务 A] --> dba[(A 库)]\n s2[服务 B] --> dbb[(B 库)]"
+ }
+ },
+ {
+ "label": "其他"
+ }
+ ]
+ },
+ "data_layer_custom": {
+ "type": "string",
+ "title": "数据层 · 自定义说明",
+ "description": "选了「其他」时填写:你的数据层长什么样、为什么前两种都不合适。",
+ "x-multiline": true,
+ "x-visible-when": {
+ "field": "data_layer",
+ "op": "equals",
+ "value": "其他"
+ }
+ },
+ "service_flow": {
+ "type": "string",
+ "title": "调用链图(可编辑)",
+ "description": "x-control: mermaid —— 字段的值就是图的源码,右侧实时预览。这里预填了按你拓扑推演的调用链;改图就是改答案,适合需要用户确认或修正的流程。",
+ "default": "flowchart TD\n client(client) --> gw[api 网关]\n gw --> svc[业务服务]\n svc --> db[(数据库)]\n svc --> cache[(缓存)]",
+ "x-control": "mermaid"
+ },
+ "network": {
+ "type": "object",
+ "title": "网络边界",
+ "description": "对象节点也能挂 x-content:这张图解释的是下面两个字段共同描述的边界,而不是哪一个字段。",
+ "x-content": {
+ "type": "mermaid",
+ "source": "flowchart LR\n net[私有子网] --- svc[服务实例]\n svc --- db[(数据库)]\n lb[公网负载均衡] --- net"
+ },
+ "properties": {
+ "exposure": {
+ "type": "string",
+ "title": "入口暴露",
+ "description": "服务如何被公网访问。",
+ "enum": [
+ "公网负载均衡",
+ "仅内网 + 跳板",
+ "其他"
+ ],
+ "default": "公网负载均衡",
+ "x-options": [
+ {
+ "label": "公网负载均衡"
+ },
+ {
+ "label": "仅内网 + 跳板"
+ },
+ {
+ "label": "其他"
+ }
+ ]
+ },
+ "exposure_custom": {
+ "type": "string",
+ "title": "入口暴露 · 自定义说明",
+ "description": "选了「其他」时填写。",
+ "x-visible-when": {
+ "field": "exposure",
+ "op": "equals",
+ "value": "其他"
+ }
+ }
+ }
+ },
+ "notes": {
+ "type": "string",
+ "title": "补充说明",
+ "description": "任何图都没表达出来的约束(合规、既有系统、团队技能)写在这里。",
+ "x-multiline": true
+ }
+ }
+}
diff --git a/examples/web-research-brief.schema.json b/examples/web-research-brief.schema.json
index c5dad0a..14fcb49 100644
--- a/examples/web-research-brief.schema.json
+++ b/examples/web-research-brief.schema.json
@@ -102,7 +102,11 @@
"maximum": 4,
"x-control": "slider",
"x-slider-marks": [0, 1, 2, 3, 4],
- "default": 2
+ "default": 2,
+ "x-content": {
+ "type": "mermaid",
+ "source": "flowchart LR\n q[检索] --> f[筛选] --> x[提炼] --> w[成文]\n f -. 浅层到此 .-> w\n x -. 深层继续 .-> v[交叉验证] --> w"
+ }
},
"source_authority": {
"type": "integer",
@@ -428,6 +432,13 @@
"description": "键值对:方便把这次调研归档和检索,例如 项目=2026 选型。",
"additionalProperties": { "type": "string" },
"default": { "项目": "2026 选型", "负责人": "产品组" }
+ },
+ "report_flow": {
+ "type": "string",
+ "title": "调研流程图(可编辑)",
+ "description": "x-control: mermaid —— 字段的值就是图的源码,右侧实时预览。这里预填了本次调研的执行流程;如果你对步骤或顺序有意见,直接改图,改的就是执行约定。",
+ "default": "flowchart TD\n q[检索 query 集] --> s[相关性打分]\n s --> dedup[去重合并]\n dedup --> read[精读 top 来源]\n read --> draft[初稿]\n draft --> cite[逐条对引用] --> final[终稿]",
+ "x-control": "mermaid"
}
}
}
diff --git a/prompts/ask.prompt.md b/prompts/ask.prompt.md
index 36887db..2fbc643 100644
--- a/prompts/ask.prompt.md
+++ b/prompts/ask.prompt.md
@@ -168,3 +168,19 @@ users to tunnel first: `ssh -L 8787:localhost:8787 `.
Before asking, check `.schemaui/answers/` — the user may have already answered
something similar. Reuse a prior answer as defaults via `--config`.
+
+## Show, don't tell (figures)
+
+- Options that differ in _shape_ (topologies, rollouts, data models) get a
+ diagram each:
+ `"x-options": [{"label": …, "description": …, "content":
+ {"type": "mermaid", "source": "flowchart …"}}, …]`
+ — aligned with `enum` by index; use `radio`/`select` (segmented cannot show
+ figures).
+- A question that needs a picture to make sense carries
+ `"x-content": {"type": "mermaid"|"svg"|"markdown", "source": …}` — rendered
+ above the control.
+- Only when the answer _is_ a diagram: `"x-control": "mermaid"` edits the source
+ with a live preview. A figure explains; it is never the value.
+- Reference: `examples/deployment-architecture.schema.json` (every option with a
+ diagram) and `schemaui ≥ 0.16` for the figure surfaces.
diff --git a/skills/grill-with-docs/SKILL.md b/skills/grill-with-docs/SKILL.md
new file mode 100644
index 0000000..5cfc6a3
--- /dev/null
+++ b/skills/grill-with-docs/SKILL.md
@@ -0,0 +1,69 @@
+---
+name: grill-with-docs
+description: >
+ Interview the user relentlessly about a plan or design until reaching shared
+ understanding — one question at a time, walking each branch of the decision
+ tree, every question carrying a recommended answer, and every answer grounded
+ in the project's own docs and examples rather than guesses. Use when the user
+ asks to be grilled, wants a plan stress-tested, or when a design has to be
+ pinned down before code is written. Built into a2ui-ask so agents that need
+ the discipline have it even when no separate grilling skill is installed.
+---
+
+# grill-with-docs — relentless interviewing, grounded in reference docs
+
+Interview the user relentlessly about every aspect of this plan until you reach
+a shared understanding. Walk down each branch of the decision tree, resolving
+dependencies between decisions one by one: a choice that constrains later
+questions gets asked before the questions it constrains. For every question,
+provide your recommended answer — the user should be confirming or correcting,
+not starting from a blank page.
+
+Ask the questions **one at a time**, and stop when the answers have converged:
+no new branches are opening, and you could write the plan down without inventing
+anything.
+
+If a question can be answered by exploring the codebase, git history, existing
+answers, or the reference docs below — explore instead of asking. The interview
+is for decisions only the user can make.
+
+## Ground every question in the docs
+
+Before the interview starts, read the reference material the task points at — a
+feature brief should be shaped by how similar features are scoped in this repo,
+a UI by the design system, a schema by the engine's own examples. Concretely,
+for forms built with [schemaui](https://github.com/YuniqueUnic/schemaui):
+
+- the control cheat sheet and rich-content guide in the a2ui-ask `SKILL.md` that
+ ships next to this file;
+- the runnable forms in a2ui-ask's `examples/` — the shortest path to "what does
+ a good schema look like";
+- schemaui's own
+ [controls gallery](https://github.com/YuniqueUnic/schemaui/blob/main/examples/controls-gallery.schema.json)
+ and
+ [rich-content gallery](https://github.com/YuniqueUnic/schemaui/blob/main/examples/rich-content.schema.json)
+ — every hint value and every figure surface side by side.
+
+Reading beats guessing: an option list drafted after looking at the gallery is
+shorter and better labelled than one drafted from memory.
+
+## Interview discipline
+
+1. **One question per turn.** The user answers in one line; you follow up in the
+ next. A wall of ten questions gets ten lazy answers.
+2. **Recommend, then ask.** "I'd go with X because Y — ok?" beats "what should
+ we do about Z?".
+3. **Resolve dependencies in order.** Ask the question that narrows the space
+ before the questions it narrows.
+4. **Explore instead of asking** whenever the answer is discoverable.
+5. **Record what you heard.** Restate the decision in one line before moving to
+ the next branch, so a misunderstanding costs one message, not a whole plan.
+6. **Stop when converged.** Ending the interview is part of the discipline.
+
+## When the interview becomes a form
+
+If the interviewer's questions are structured choices and config, stop asking in
+chat: generate a draft-07 JSON Schema with your recommended answers as
+`default`s and hand it to a2ui-ask's ask script, so the user gets a real form
+(see the a2ui-ask `SKILL.md` for the control cheat sheet). Use the form for the
+cluster of decisions; keep the chat interview for the open-ended ones.
diff --git a/tests/test_ask.py b/tests/test_ask.py
index f307272..249c4b0 100644
--- a/tests/test_ask.py
+++ b/tests/test_ask.py
@@ -90,6 +90,7 @@
"/urgency": "segmented",
"/include_competitor_pricing": "checkbox",
"/accent_color": "color",
+ "/report_flow": "mermaid",
}
# The other half of that contract: a hint is opt-in, so these fields must stay
@@ -291,18 +292,47 @@ def read_until(proc: subprocess.Popen, marker: str, timeout: float = 30) -> str:
raise AssertionError(f"never saw {marker!r} on stdout")
+def _get_first(url: str, paths: tuple[str, ...]):
+ """GET the first path that answers; schemaui ≥ 0.9 serves /api/v1/*, older
+ engines the unversioned routes. A 404 falls through, anything else raises."""
+ last: Exception | None = None
+ for path in paths:
+ try:
+ return urllib.request.urlopen(f"{url}{path}", timeout=5)
+ except urllib.error.HTTPError as error:
+ if error.code == 404:
+ last = error
+ continue
+ raise
+ raise last # type: ignore[misc]
+
+
+def _post_first(url: str, paths: tuple[str, ...], body: bytes):
+ last: Exception | None = None
+ for path in paths:
+ req = urllib.request.Request(
+ f"{url}{path}",
+ data=body,
+ headers={"Content-Type": "application/json"},
+ method="POST",
+ )
+ try:
+ return urllib.request.urlopen(req, timeout=5)
+ except urllib.error.HTTPError as error:
+ if error.code == 404:
+ last = error
+ continue
+ raise
+ raise last # type: ignore[misc]
+
+
def drive_session(url: str, payload: dict) -> None:
"""Play the role of the user: load the session, then Save & Exit."""
url = url.rstrip("/")
- with urllib.request.urlopen(f"{url}/api/session", timeout=5) as resp:
+ with _get_first(url, ("/api/v1/session", "/api/session")) as resp:
assert resp.status == 200
- req = urllib.request.Request(
- f"{url}/api/exit",
- data=json.dumps({"data": payload, "commit": True}).encode(),
- headers={"Content-Type": "application/json"},
- method="POST",
- )
- with urllib.request.urlopen(req, timeout=5) as resp:
+ body = json.dumps({"data": payload, "commit": True}).encode()
+ with _post_first(url, ("/api/v1/exit", "/api/exit"), body) as resp:
assert resp.status == 200
@@ -410,7 +440,7 @@ def test_e2e_feature_brief_full_control_showcase(tmp_path: Path):
)
try:
url = read_until(proc, "SCHEMAUI_URL=").strip().split("=", 1)[1].rstrip("/")
- with urllib.request.urlopen(f"{url}/api/session", timeout=5) as resp:
+ with _get_first(url, ("/api/v1/session", "/api/session")) as resp:
session = json.loads(resp.read())
kinds = [
next(iter(root["kind"].values()) if isinstance(root["kind"], dict) else root["kind"])
@@ -479,7 +509,7 @@ def test_e2e_web_research_brief_covers_the_control_gallery(tmp_path: Path):
)
try:
url = read_until(proc, "SCHEMAUI_URL=").strip().split("=", 1)[1].rstrip("/")
- with urllib.request.urlopen(f"{url}/api/session", timeout=5) as resp:
+ with _get_first(url, ("/api/v1/session", "/api/session")) as resp:
session = json.loads(resp.read())
by_pointer = {node["pointer"]: node for node in collect_nodes(session["ui_ast"]["roots"])}
@@ -497,6 +527,11 @@ def bounds(pointer: str) -> dict:
# ... and `x-multiline` marks a string multi-line without naming a control
assert by_pointer["/background"]["kind"]["multiline"] is True
+ # figures: the depth slider carries a node-level mermaid x-content, and
+ # the report flow is edited as a live-preview mermaid source
+ assert by_pointer["/depth_level"]["content"]["type"] == "mermaid"
+ assert by_pointer["/report_flow"]["control"] == "mermaid"
+
# the slider/range variants the gallery separates: a derived step
# against a declared one, and bare against labelled against unit marks
assert bounds("/relevance_floor")["step"] is None