design-playbook 0.24.0 → 0.24.2

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/codex/AGENTS.md CHANGED
@@ -1,4 +1,4 @@
1
- <!-- generated-by design-playbook v0.24.0 -->
1
+ <!-- generated-by design-playbook v0.24.2 -->
2
2
  # design-playbook for Codex
3
3
 
4
4
  ## Install (path of record)
@@ -26,3 +26,6 @@ Registry: `skills/design-playbook/references/rules.md`, full catalog (P3 run:
26
26
  | DECIDE-01@1 | applicable | - | clear | 选中方向为全局 run console 构成重组(candidates/console-region.html 草图 + preview round 1/2 用户确认),非未审视的默认外观收敛 | DD-0001 理由可回溯 l1.scenes(切页续读)与 PERF-01 比较轴及经用户确认的布局段突破;DD-0002 理由可回溯 l6.c4(跨视图状态闭环)与运行中心第一步方向——均引用 brief 具体事实 | 常规方向经比较矩阵沿 brief 轴证成,非未审视默认;无基线默认方向身份声明 | - |
27
27
 
28
28
  注:2026-08-28 注册批(COPY-01/02/03、A11Y-02、CRAFT-09/10、DECIDE-01)晚于本 run 存档;按三态谓词补记——blocked 行在理由列点名缺失的证据面;DECIDE-01 依归档内可读的决策报告求值为 applicable。
29
+ | STATE-01@1 | applicable | - | clear | 运行 feed 条目级进度持续更新(进行中项逐条指名) | Fill 声明条目级进度 + evidence 进度语义 | - | - |
30
+ | STATE-02@1 | applicable | - | clear | 「无运行时 feed 不渲染」为 spec 声明边界:无 feed 时控制台不呈现空集合区 | Fill 声明边界条目(L 边界对齐) | 例外成立——空呈现即 spec 声明的产品行为 | - |
31
+ | STATE-03@1 | applicable | - | clear | 暂停失败 toast(role=alert)+ 失败项保留并支持圈选批量重试 | Fill 声明(失败 toast + 批量重试操作条) | - | - |
@@ -26,3 +26,6 @@ Registry: `skills/design-playbook/references/rules.md`, full catalog (P2 run). S
26
26
  | DECIDE-01@1 | applicable | - | clear | 选中方向为按周命名模式(compare 档轻量比较),不落入 2026-08 记录的自默认外观样貌;DD-0001 为 record 档,不在本规则范围 | DD-0002 理由可回溯 l1.target_user 比较轴(周频归档场景下检索是主任务)——引用 brief 具体事实而非通用措辞 | 常规方向经比较矩阵沿 brief 轴证成,非未审视默认;无基线默认方向身份声明 | - |
27
27
 
28
28
  注:2026-08-28 注册批(COPY-01/02/03、A11Y-02、CRAFT-09/10、DECIDE-01)晚于本 run 存档;按三态谓词补记——blocked 行在理由列点名缺失的证据面;DECIDE-01 依归档内可读的决策报告求值为 applicable。
29
+ | STATE-01@1 | applicable | - | clear | 批量导出触发钮 busy/disabled 守卫(控件自身标签即操作名,pending 区域限于触发钮) | Fill 声明 busy state while exporting + busy/disabled guard | 单帧完成例外不成立(批量导出为长操作) | - |
30
+ | STATE-02@1 | blocked | 静态夹具无数据源,首载空/筛选空均不可达 | - | 表格列表区声明在场,空态分支缺席 | 无数据源可供触发空态 | - | 接入数据面后补评首载空与筛选空呈现 |
31
+ | STATE-03@1 | applicable | - | clear | cap 上限 toast(role=alert + 可读名称含上限值) | Fill 声明 toast 承载上限拒绝 | 上限拒绝为失败路径的可恢复呈现(出口=调整范围后再导出) | - |
@@ -26,3 +26,6 @@ Registry: `skills/design-playbook/references/rules.md`. Seven-column rows; after
26
26
  | DECIDE-01@1 | not-applicable | 决策报告仅含 record 档 DD-0101,无 compare/explore 档方向决策条目 | - | - | - | - | - |
27
27
 
28
28
  注:2026-08-28 注册批(COPY-01/02/03、A11Y-02、CRAFT-09/10、DECIDE-01)晚于本 run 存档;按三态谓词补记,blocked 行在理由列记缺失证据面。
29
+ | STATE-01@1 | applicable | - | clear | 导出触发钮 busy/disabled 守卫(自前次 run 保留) | Fill 声明 busy/disabled guard 保留 | 单帧完成例外不成立 | - |
30
+ | STATE-02@1 | applicable | - | clear | 空数据集预检:数据集为空时触发钮禁用 + toast「无可选行」(指名原因) | Fill 声明 empty-blocked pre-check(R4 修复) | 例外不成立——空数据呈现明确指名原因与出口 | - |
31
+ | STATE-03@1 | applicable | - | clear | cap 上限 toast(自前次 run 保留,role=alert) | Fill 声明 cap-limit toast 保留 | - | - |
@@ -26,3 +26,6 @@ Registry: `skills/design-playbook/references/rules.md`, full catalog (P3 run:
26
26
  | DECIDE-01@1 | applicable | - | clear | 选中方向为启用既有 status region 收纳导出任务(candidates/B.html 草图 + preview round 1/2 用户确认),非未审视的默认外观收敛 | DD-0003 理由可回溯 l1.scenes(导出中切页全局可查)与 PERF-01 比较轴;DD-0004 理由可回溯 l6.c2(跨视图状态闭环)与基线 status region 惯例声明——均引用 brief 具体事实 | 常规方向经比较矩阵沿 brief 轴证成,非未审视默认;基线声明的是 status region 惯例而非默认外观身份 | - |
27
27
 
28
28
  注:2026-08-28 注册批(COPY-01/02/03、A11Y-02、CRAFT-09/10、DECIDE-01)晚于本 run 存档;按三态谓词补记——blocked 行在理由列点名缺失的证据面;DECIDE-01 依归档内可读的决策报告求值为 applicable。
29
+ | STATE-01@1 | applicable | - | clear | 长导出条目级进度持续更新(30s 窗口 5 次采样) | evidence/L6.1-status-trace.json 进度采样序列 | - | - |
30
+ | STATE-02@1 | blocked | 候选页为静态导出面,无数据供给集合面,空态不可达 | - | 无空态分支呈现 | 无数据源 | - | 接入数据面后补评 |
31
+ | STATE-03@1 | not-applicable | 本 run 升档面未声明异步失败路径(进度轨迹为唯一运行时面) | - | - | 候选页无可触发失败分支 | - | - |
@@ -139,6 +139,29 @@ class RunConsoleRequestHandler(http.server.BaseHTTPRequestHandler):
139
139
  def log_message(self, format: str, *args: object) -> None:
140
140
  """Never log: no URL, token, or locator may reach any log."""
141
141
 
142
+ def finish(self) -> None:
143
+ """Graceful teardown: half-close, drain, then close.
144
+
145
+ A client body left unread in the receive buffer (e.g. a POST whose
146
+ Content-Length is absent: the stray bytes parse as garbage request
147
+ lines and the connection must die) turns a plain close() into a TCP
148
+ reset on Windows — the client gets WinError 10053 mid-read instead
149
+ of the response it was sent. Flush the response, half-close (FIN),
150
+ drain the peer's remaining bytes, then close, so teardown is never
151
+ a reset.
152
+ """
153
+ super().finish()
154
+ try:
155
+ self.connection.shutdown(socket.SHUT_WR)
156
+ except OSError:
157
+ pass
158
+ try:
159
+ self.connection.settimeout(0.2)
160
+ while self.connection.recv(65536):
161
+ pass
162
+ except OSError:
163
+ pass
164
+
142
165
  # -- dispatch ------------------------------------------------------
143
166
 
144
167
  def do_GET(self) -> None: # noqa: N802
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "design-playbook",
3
- "version": "0.24.0",
3
+ "version": "0.24.2",
4
4
  "description": "Design I/O for coding agents: controllable UI generation via declarations (spec/domain/craft/design/components/template) and contracts (skill/evaluator). Use for product UI—console, dashboard, agent-ops, CJK-first apps.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -115,6 +115,16 @@ _TIER2: tuple[AgentRow, ...] = (
115
115
  skills=False,
116
116
  rules_target=".github/copilot-instructions.md",
117
117
  ),
118
+ AgentRow(
119
+ agent="zed",
120
+ tier=2,
121
+ rules=True,
122
+ commands=False,
123
+ mcp_project=True,
124
+ hooks=False,
125
+ skills=False,
126
+ rules_target=".rules + .zed/settings.json",
127
+ ),
118
128
  )
119
129
 
120
130
  # Tier 3 — rules floor (generated AGENTS.md + inline MCP guide).
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env python3
2
+ """Context budget for design-playbook skills (report-only).
3
+
4
+ Walks ``skills/*/SKILL.md`` and ``skills/*/references/*.md`` under the
5
+ package root and reports the context weight of the skill surface: the
6
+ always-loaded SKILL.md face per skill, its on-demand references, and the
7
+ worst-case full-pipeline total. Purely informational — no thresholds, no
8
+ gates, no writes, no network.
9
+
10
+ Token counts are character-level estimates, never tokenizer-exact:
11
+ ``ascii_chars / 4 + non_ascii_chars`` (CJK prose runs near one token per
12
+ character; every output carries ``estimate: true``).
13
+
14
+ Usage:
15
+ python scripts/context_budget.py # text view (package root)
16
+ python scripts/context_budget.py --json # machine view
17
+ python scripts/context_budget.py --root <package-root> --top 5
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import json
23
+ import sys
24
+ from pathlib import Path
25
+
26
+ _PACKAGE_ROOT = Path(__file__).resolve().parent.parent
27
+
28
+ _ESTIMATE_NOTE = (
29
+ "token counts are character-level estimates: ascii/4 + non-ascii chars; "
30
+ "not tokenizer-exact"
31
+ )
32
+
33
+
34
+ def estimate_tokens(text: str) -> int:
35
+ """Character-level estimate (see module docstring)."""
36
+ non_ascii = sum(1 for ch in text if ord(ch) > 127)
37
+ return (len(text) - non_ascii) // 4 + non_ascii
38
+
39
+
40
+ def _measure_file(path: Path) -> dict:
41
+ text = path.read_text(encoding="utf-8", errors="replace")
42
+ chars = len(text)
43
+ return {
44
+ "path": path.name,
45
+ "chars": chars,
46
+ "tokens_est": estimate_tokens(text),
47
+ }
48
+
49
+
50
+ def measure_skills(package_root: Path) -> dict:
51
+ """Measure the whole skill surface under *package_root*. Read-only."""
52
+ skills_dir = package_root / "skills"
53
+ skills: list[dict] = []
54
+ for skill_dir in sorted(p for p in skills_dir.iterdir() if p.is_dir()):
55
+ skill_file = skill_dir / "SKILL.md"
56
+ if not skill_file.is_file():
57
+ continue
58
+ skill = {
59
+ "skill": skill_dir.name,
60
+ "skill_md": _measure_file(skill_file),
61
+ "references": [
62
+ _measure_file(p) for p in sorted((skill_dir / "references").glob("*.md"))
63
+ ],
64
+ }
65
+ skill["references_tokens_est"] = sum(
66
+ ref["tokens_est"] for ref in skill["references"]
67
+ )
68
+ skill["skill_tokens_est"] = skill["skill_md"]["tokens_est"]
69
+ skill["total_tokens_est"] = (
70
+ skill["skill_tokens_est"] + skill["references_tokens_est"]
71
+ )
72
+ skills.append(skill)
73
+
74
+ all_files = [
75
+ {"skill": s["skill"], **ref}
76
+ for s in skills
77
+ for ref in s["references"]
78
+ ] + [{"skill": s["skill"], **s["skill_md"]} for s in skills]
79
+ heaviest = sorted(all_files, key=lambda f: f["tokens_est"], reverse=True)
80
+
81
+ return {
82
+ "estimate": True,
83
+ "estimate_note": _ESTIMATE_NOTE,
84
+ "skills": skills,
85
+ "total": {
86
+ "skill_md_tokens_est": sum(s["skill_tokens_est"] for s in skills),
87
+ "references_tokens_est": sum(s["references_tokens_est"] for s in skills),
88
+ "worst_case_pipeline_tokens_est": sum(s["total_tokens_est"] for s in skills),
89
+ },
90
+ "heaviest_files": heaviest[:10],
91
+ }
92
+
93
+
94
+ def _text_view(report: dict) -> str:
95
+ lines = [
96
+ "context budget (estimate: %s)" % report["estimate"],
97
+ report["estimate_note"],
98
+ "",
99
+ f"{'skill':<18} {'skill.md':>10} {'refs':>10} {'total':>10}",
100
+ ]
101
+ for s in report["skills"]:
102
+ lines.append(
103
+ f"{s['skill']:<18} {s['skill_tokens_est']:>10} "
104
+ f"{s['references_tokens_est']:>10} {s['total_tokens_est']:>10}"
105
+ )
106
+ t = report["total"]
107
+ lines += [
108
+ "",
109
+ f"{'TOTAL':<18} {t['skill_md_tokens_est']:>10} "
110
+ f"{t['references_tokens_est']:>10} "
111
+ f"{t['worst_case_pipeline_tokens_est']:>10}",
112
+ "",
113
+ "heaviest files (tokens_est):",
114
+ ]
115
+ for f in report["heaviest_files"]:
116
+ lines.append(f" {f['tokens_est']:>8} {f['skill']}/{f['path']}")
117
+ return "\n".join(lines)
118
+
119
+
120
+ def main(argv: list[str] | None = None) -> int:
121
+ parser = argparse.ArgumentParser(
122
+ prog="context_budget",
123
+ description="Report the context weight of the skill surface (report-only).",
124
+ )
125
+ parser.add_argument(
126
+ "--root",
127
+ default=str(_PACKAGE_ROOT),
128
+ help="package root holding skills/ (default: this package)",
129
+ )
130
+ parser.add_argument("--json", action="store_true", help="machine-readable output")
131
+ parser.add_argument("--top", type=int, default=10, metavar="N",
132
+ help="heaviest-file list length (default 10)")
133
+ args = parser.parse_args(argv)
134
+
135
+ root = Path(args.root).resolve()
136
+ report = measure_skills(root)
137
+ report["heaviest_files"] = report["heaviest_files"][: max(args.top, 0)]
138
+
139
+ if args.json:
140
+ print(json.dumps(report, ensure_ascii=False, indent=2))
141
+ else:
142
+ print(_text_view(report))
143
+ return 0
144
+
145
+
146
+ if __name__ == "__main__":
147
+ sys.exit(main())
package/scripts/doctor.py CHANGED
@@ -59,6 +59,7 @@ _ORPHAN_SCAN_DIRS: dict[str, tuple[str, ...]] = {
59
59
  "gemini-cli": (".gemini/commands",),
60
60
  "windsurf": (".windsurf/rules", ".windsurf/workflows"),
61
61
  "github-copilot": (".github/instructions",),
62
+ "zed": (".zed",),
62
63
  }
63
64
  # Whole-file candidates carrying the generated-by marker outside namespaced
64
65
  # dirs (marker-block targets are always re-rendered in place, so they are
@@ -68,6 +69,7 @@ _WHOLE_FILE_CANDIDATES = (
68
69
  "GEMINI.md",
69
70
  ".github/copilot-instructions.md",
70
71
  "design-playbook-mcp-setup.md",
72
+ ".rules",
71
73
  )
72
74
 
73
75
  _LIMITATIONS = (
@@ -649,6 +649,60 @@ def _agents_md_floor_files(version: str, out_dir: Path) -> list[tuple[str, str]]
649
649
  return [_marker_entry(out_dir, "AGENTS.md", version, "".join(block_parts))]
650
650
 
651
651
 
652
+ # ---------------------------------------------------------------------------
653
+ # Zed renderer (Tier 2)
654
+ # ---------------------------------------------------------------------------
655
+
656
+ # Zed reads project-root rules via a first-match priority list with `.rules`
657
+ # on top, then `.cursorrules`, `.windsurfrules`, `.clinerules`,
658
+ # `.github/copilot-instructions.md`, `CLAUDE.md`, `AGENTS.md`, … (zed.dev
659
+ # agent rules docs, fetched 2026-09-19). Creating `.rules` when a
660
+ # lower-priority competitor exists would silently shadow the user's own
661
+ # rules, so the renderer refuses to introduce one into that state.
662
+ _ZED_RULES_COMPETITORS = (
663
+ ".cursorrules",
664
+ ".windsurfrules",
665
+ ".clinerules",
666
+ "CLAUDE.md",
667
+ ".github/copilot-instructions.md",
668
+ )
669
+
670
+
671
+ def _zed_files(version: str, out_dir: Path) -> list[tuple[str, str]]:
672
+ files: list[tuple[str, str]] = []
673
+ skills = _read_skills()
674
+
675
+ # `.rules` — marker-block (may pre-exist). Skip creating a *new* `.rules`
676
+ # while a documented lower-priority competitor is present; an existing
677
+ # `.rules` (ours or the user's) takes refresh/append semantics instead,
678
+ # since Zed already reads that exact file.
679
+ rules_existing = _existing_text(out_dir, ".rules")
680
+ if rules_existing is not None or not any(
681
+ (out_dir / c).exists() for c in _ZED_RULES_COMPETITORS
682
+ ):
683
+ block_parts = _digest_head(skills)
684
+ files.append(_marker_entry(out_dir, ".rules", version, "".join(block_parts)))
685
+
686
+ # .zed/settings.json — merge-safe context_servers (project-level MCP).
687
+ # Official docs (fetched 2026-09-19) document the stdio entry as
688
+ # {"command": "…", "args": […], "env": {…}}; community examples also show
689
+ # an object form ({"command": {"path": …, "args": […]}}) — the string
690
+ # form is pinned here per the official page, and the merge keeps any
691
+ # user-managed entries verbatim.
692
+ mcp_servers = _mcp_servers_abs()
693
+ zed_servers: dict = {}
694
+ for name, srv in mcp_servers.items():
695
+ entry: dict = {"command": srv["command"], "args": srv["args"]}
696
+ if "env" in srv and any(v for v in srv["env"].values()):
697
+ entry["env"] = srv["env"]
698
+ zed_servers[name] = entry
699
+ files.append(
700
+ _merge_json_entry(out_dir, ".zed/settings.json", {"context_servers": zed_servers})
701
+ )
702
+
703
+ return files
704
+
705
+
652
706
  # ---------------------------------------------------------------------------
653
707
  # Renderer dispatch
654
708
  # ---------------------------------------------------------------------------
@@ -664,6 +718,7 @@ _SPECIALIZED_RENDERERS: dict[str, Renderer] = {
664
718
  "opencode": _opencode_files,
665
719
  "windsurf": _windsurf_files,
666
720
  "github-copilot": _github_copilot_files,
721
+ "zed": _zed_files,
667
722
  }
668
723
 
669
724
 
@@ -1,6 +1,6 @@
1
1
  # Craft audit protocol (registry reference layer)
2
2
 
3
- The craft detectors live in the first-party rule registry: [`../../design-playbook/references/rules.md`](../../design-playbook/references/rules.md) (`CRAFT-01` … `CRAFT-08`, all `advisory` / `first-party`, plus the cross-cutting `A11Y-01`, `RESP-01`, and the placeholder `I18N-01`, `PERF-01`, `SEC-01` entries). This file is the thin execution reference: how to evaluate an entry's applicability predicate and how to write its audit row. The registry is the single authority for detector definitions — do not duplicate entry text here.
3
+ The craft detectors live in the first-party rule registry: [`../../design-playbook/references/rules.md`](../../design-playbook/references/rules.md). The registry is the single authority for entry definitions — this file never enumerates or summarizes entries (enumerations here have drifted before; read the registry for the current set, families, statuses, and provenance). This file is the thin execution reference: how to evaluate an entry's applicability predicate and how to write its audit row.
4
4
 
5
5
  Run every registry craft entry whose applicability predicate evaluates to `applicable` for implemented UI. Inspect rendered UI at declared target viewports and relevant source. Generic registry outcomes never override a verified project baseline; safety, usability, and explicit declarations still do.
6
6
 
@@ -607,3 +607,93 @@ fix: answer the self-check inside the DD rationale with brief facts, add at leas
607
607
  related:
608
608
  history: 1 | 2026-08-28 | docs | initial registry registration, first-party decision-hygiene entry; default-direction examples recorded as dated, refreshable observations (2026-08, dogfood evidence)
609
609
  ```
610
+
611
+ ## STATE-01 — Pending feedback
612
+
613
+ ```yaml
614
+ id: STATE-01
615
+ version: 1
616
+ title: Pending feedback
617
+ statement: Asynchronous triggers and data-loading regions present pending feedback that names the operation while it runs (observable); a silent trigger or a bare anonymous spinner leaves users guessing whether the action registered and whether waiting is safe (user impact).
618
+ capability-domain: D4
619
+ executes-in: D4:cross-cutting
620
+ authority: advisory-aesthetic
621
+ applicability-applicable: run has Fill output and the surface contains asynchronous triggers or data-loading regions (form submits, fetch-driven lists, uploads)
622
+ applicability-not-applicable: planning-only run, or every visible operation completes synchronously within the interaction (observable reason required)
623
+ applicability-blocked: pending states cannot be reached or captured with the available evidence surface
624
+ check-type: protocol-check
625
+ check-inputs: rendered pending states per asynchronous path; source request and state handling that gates the pending branch
626
+ signals-rendered: an asynchronous trigger with no visible pending feedback; a pending region that does not name what is loading or processing (a page-wide bare spinner covering one field's request)
627
+ signals-source: request handling with no pending branch bound to a rendered region; a pending flag consumed by nothing visible
628
+ evidence-layers: rendered>=1, source>=1
629
+ evidence-method: expert-review
630
+ severity-default: S2 / fact
631
+ exceptions: operations that complete and surface their result within a single frame on the audited device (the pending state would only flash); background prefetch not tied to a visible user task
632
+ false-positives: progress already carried by a dedicated inline region that names the operation; a verified baseline prescribing silent local mutations
633
+ owner: craft -> R4
634
+ provenance: benchmark-input-only
635
+ status: advisory
636
+ fix: bind each asynchronous path to a pending region that names the operation, keep it scoped to the region it concerns, and resolve it deterministically into the result or failure state (PERF-01 covers the pacing of the feedback itself)
637
+ related: PERF-01@1
638
+ history: 1 | 2026-09-19 | docs | initial registry registration, benchmark-informed state-completeness entry in first-party wording
639
+ ```
640
+
641
+ ## STATE-02 — Zero-data presentation
642
+
643
+ ```yaml
644
+ id: STATE-02
645
+ version: 1
646
+ title: Zero-data presentation
647
+ statement: Data-fed collection surfaces declare what users see when the list is empty on first load and after filtering (observable); a blank region forces users to guess whether data is still loading, absent, or broken, and hides the next action (user impact).
648
+ capability-domain: D4
649
+ executes-in: D4:cross-cutting
650
+ authority: advisory-aesthetic
651
+ applicability-applicable: run has Fill output and the surface contains a collection or list display fed by data
652
+ applicability-not-applicable: planning-only run, or no data-fed collection display in the audited scope (observable reason required)
653
+ applicability-blocked: empty states cannot be reached or captured with the available evidence surface
654
+ check-type: protocol-check
655
+ check-inputs: rendered first-load-empty and filtered-empty presentations; source empty-branch handling for the collection's data source
656
+ signals-rendered: the collection region renders with no content and no hint while its data is empty; an empty presentation that offers no next action or explanation
657
+ signals-source: empty-result branches that render no region content; a data source with an empty path unhandled in the UI layer
658
+ evidence-layers: rendered>=1, source>=1
659
+ evidence-method: expert-review
660
+ severity-default: S2 / fact
661
+ exceptions: create-first surfaces whose documented initial state is intentionally the empty canvas itself; the empty presentation is the specified product behavior (spec or verified baseline)
662
+ false-positives: a populated skeleton resolving within the declared loading flow (judged under STATE-01, not here)
663
+ owner: craft -> R4
664
+ provenance: benchmark-input-only
665
+ status: advisory
666
+ fix: give every data-fed collection an explicit empty presentation that says what the region is for and offers the next action, visually distinct from the pending presentation (STATE-01) and from failure feedback (STATE-03)
667
+ related: STATE-01@1, STATE-03@1
668
+ history: 1 | 2026-09-19 | docs | initial registry registration, benchmark-informed state-completeness entry in first-party wording
669
+ ```
670
+
671
+ ## STATE-03 — Failure feedback
672
+
673
+ ```yaml
674
+ id: STATE-03
675
+ version: 1
676
+ title: Failure feedback
677
+ statement: Failed asynchronous paths present recoverable feedback that names what failed and the next action — retry, undo, or exit — instead of failing silently to the console or surfacing a raw exception trace (observable); users stranded at a failure without a recovery path abandon the flow or retrigger the damage (user impact).
678
+ capability-domain: D4
679
+ executes-in: D4:cross-cutting
680
+ authority: advisory-aesthetic
681
+ applicability-applicable: run has Fill output and the audited scope declares or triggers failure paths for asynchronous operations
682
+ applicability-not-applicable: run has no asynchronous failure path in the audited scope (observable reason required)
683
+ applicability-blocked: failure states cannot be reached or captured with the available evidence surface
684
+ check-type: protocol-check
685
+ check-inputs: rendered failure states per asynchronous failure path; source error handling bound to request or mutation paths
686
+ signals-rendered: an asynchronous action fails with no rendered feedback beyond the previous state; a raw exception or stack trace rendered to the user; a failure banner offering no retry, undo, or exit action
687
+ signals-source: error branches that only log to the console; a catch path with no bound rendered region or recovery action
688
+ evidence-layers: rendered>=1, source>=1
689
+ evidence-method: expert-review
690
+ severity-default: S2 / fact
691
+ exceptions: diagnostic or operator surfaces where raw detail is the declared point (spec L1 or verified baseline); security-sensitive withholdings — still state what the user can do next even when the cause is withheld
692
+ false-positives: failure feedback deferred by a declared retry policy that still lands in a rendered recovery state; terse diagnostics for a declared expert audience (spec L1)
693
+ owner: craft -> R4
694
+ provenance: benchmark-input-only
695
+ status: advisory
696
+ fix: bind each failure path to a rendered recovery region naming what failed where safe and what to do next, with retry or exit always present; keep the tone even (COPY-03 covers the message wording)
697
+ related: COPY-03@1, STATE-01@1
698
+ history: 1 | 2026-09-19 | docs | initial registry registration, benchmark-informed state-completeness entry in first-party wording
699
+ ```