fix(ppt,tools): svg 质量门末尾出 [GATE FAIL] 汇总行——失败签名从误标"Font issues"变诚实(bump 0.58.47)

工具失败面板 shell/exit 第1簇「Font issues x20」是误标:svg_quality_checker
按设计 exit-1(质量门),但 core/toolfail 取 [exit] 前尾行当签名,而尾行永远是
通用 tip「4. Font issues:…」→ 所有门失败(typography/alignment/spec_lock drift)
全塌成一条"Font issues",误导排查方向。多文件门也没有可靠尾行(最后文件可能只有 WARN)。

修在工具侧(toolfail 保持通用零改动):
- svg_quality_checker.gate_verdict_line():errors>0 时在 main() 末尾(export 之后)
  多打一行确定性汇总 [GATE FAIL] svg_quality_checker: N error file(s) / M total;
  top issues: … 作为最后一行 stdout,toolfail 现有取尾行逻辑自动拾取诚实签名,
  模型也多一行有用汇总。
- _categorize_issue 加 spec_lock/typography 类,避免真错(typography px)落 "Other"。
- 补 GateVerdictLineTests(3):无 error 不出行 / [GATE FAIL] 打头带 top issues /
  是最后一行 stdout。56 测试全绿。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-07-20 15:59:09 +08:00
parent ab55be4cde
commit 795161adcd
4 changed files with 75 additions and 3 deletions

View File

@ -2,7 +2,7 @@
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`
最后更新:2026-07-20(文件预览背景撤回毛玻璃、改回半透明压暗遮罩,bump 0.58.46)
最后更新:2026-07-20(工具失败面板巡检:salvage 复盘=保持 + svg 质量门签名诚实化,bump 0.58.47)
---
@ -23,6 +23,7 @@
### 2026-07
- **07-20 / 0.58.47 / 工具失败面板 6 簇巡检 + svg 质量门签名诚实化**:扫近7天失败聚集逐簇定根因。①**write/edit malformed(45/32)**=100% DeepSeek v4 wire 把并行 tool_call 的 arguments 拼成 `{...}{...}`,salvage(0.58.24)复盘提前到 07-20:命中率稳 ~85%(post-launch salvaged 252/malformed 44)、残差涉事 task 全在跑无终态失败=落非流式重试自愈,**决策保持现状不再写代码**(面板绝对数被上线前 07-13/14 尾巴撑高,要看 salvaged/malformed 比)。②**shell/exit "Font issues"(20)**=`svg_quality_checker` 质量门按设计 exit-1,但 `toolfail._classify``[exit]` 前尾行当签名,而尾行永远是通用 tip「4. Font issues:…」→ 所有门失败(typography/alignment/spec_lock drift)误标成 Font issues 误导排查。修:checker 在 errors>0 时末尾多打确定性 `gate_verdict_line()`「[GATE FAIL] svg_quality_checker: N error file(s) / M total; top issues: …」作为最后一行 stdout(放 main() 里 export 之后),toolfail 现有取尾行逻辑自动拾取诚实签名、toolfail 零改动;`_categorize_issue` 加 `spec_lock/typography` 类避免真错落 "Other"。补 3 测试(GateVerdictLineTests),39+3 全绿。③**run_python 预检(31)**=pysyntax 按设计拦中文正文硬拼进 .py(引号/缩进崩),非平台 bug=提示词治理待办。④**empty(15)**=主角换成 `glm.pro52`(11/15,非旧 opus48 案),需生产跑 narrated 探针定层(本机 import litellm 卡,task e7c2f50b 备用)。⑤**web_fetch(6)**=外部 403 拒抓为主,非平台锅。
- **07-20 / 0.58.45 / glob 绝对路径崩溃(工具失败聚集 #2)**:巡检近7天工具失败聚集,`glob` 排第2(3次/3task/3用户、近24h 仍在发)。根因:`tools/fs.py` GlobTool 直接 `base.glob(pattern)`,模型传绝对 pattern(沙箱 cwd 就是 `/workspace`,自然写 `/workspace/**/*.py`)时 pathlib 抛 `NotImplementedError: Non-relative patterns are unsupported`,整个调用崩返 `[Error executing glob]`。改:绝对 pattern 先 `_split_abs_glob` 拆成「存在的静态前缀目录 + 相对 glob 串」(左起吃掉不含 `*?[` 的组件当 base)再匹配;补首字符 `/`\`\\` 判定(POSIX 前导斜杠在 Windows 宿主 `is_absolute()` 为 False,沙箱是 Linux,两处对齐)。顺手删掉原 `if "**"`/`else` 两分支代码完全相同的死重构。`/sandbox/tools/` 是 Dockerfile `COPY tools/` 拷贝,**需重建沙箱镜像**才生效。
- **07-20 / 0.58.45 / PPT 页脚漂移治本(工具失败聚集 #1,A+C)**:`shell/exit` 排第1(近7天16条 exit-1、近24h 6条,9task/7用户),实测拆解 15/16 是**逐页手写绝对坐标偏离 `spec_lock.layout_grid` 锁定值 216px 的「想对齐没对齐」**(footer_y 近失 27行为最大头,margin_x/content_top 各7行,content_bottom 溢出12行),字体/spec_lock 只是 WARN 不 fail。关键发现:`executor-base.md §180-185` 早有「HARD—checker-enforced」的 snap 纪律却仍被漂移,加 prose 无用,治本须机械化。**A**:spec_lock_reference 新增 `## footer` 锁定片段——x/y 用 `margin_x`/`footer_y` 预填好、页码 `text-anchor="end"``canvas_wmargin_x`、留 `{PAGE}` 占位,executor 每页**逐字粘贴只换页码**,把 footer_y 这个跨页漂移第一大头从「每页手算」收敛成「一次编写」(executor-base 加对应硬规则,SKILL.md 同步);冒烟验证锁定片段过质检 errors=0、无 footer_y/margin_x 报错。**C**:`svg_quality_checker._ALIGN_TOL` 2→3px(2px 对手写实测过严),同步刷 SKILL/spec_lock_reference/executor-base 的「215px/212px」表述与测试值(margin drift 用例 60/63/66→60/64/68,30 测试全绿)。content_bottom 溢出(装太多项,另一性质)与 grid-snap 后处理(B)本轮未做。
- **07-19 / 0.58.43**:文件预览遮罩改整屏、消除"上暗下亮"割裂——旧实现用 `#file-preview-modal { bottom: var(--preview-bottom-inset) }` 把遮罩顶到 chat-form 之上,好让预览时能边看边打字,代价是底部整条(输入区+左栏入口+右栏+状态)全不压暗,与上方压暗区形成一条硬边界的通亮带,很割裂。改为遮罩铺满整屏(`bottom`→`padding-bottom`,仍让居中卡片停在输入区之上不遮挡),底部随整屏统一压暗;仅 chat-form 由新 `body.fp-open``position:relative;z-index:95`(压过遮罩 z90,层叠上下文已核:`#app`/`#pane-mid` 均不建 context)+ 上向阴影,上浮成"悬浮活动条",继续可打字。预览时左栏/右栏/状态点击落到遮罩=关预览(符合"背景不可用"预期)。改 `web/static/dev.html` + `web/static/js/preview.js`(open/close 加/去 `fp-open`)。

View File

@ -1,3 +1,3 @@
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
# 改版本只动这一行。
__version__ = "0.58.46"
__version__ = "0.58.47"

View File

@ -20,7 +20,7 @@ import re
import json
import html
from pathlib import Path
from typing import List, Dict, Tuple
from typing import List, Dict, Tuple, Optional
from collections import Counter, defaultdict
from xml.etree import ElementTree as ET
@ -1095,6 +1095,8 @@ class SVGQualityChecker:
return 'Alignment/grid'
elif error_msg.startswith('Geometry:'):
return 'Geometry'
elif 'spec_lock' in error_msg or 'typography' in error_msg:
return 'spec_lock/typography'
elif 'font' in error_msg.lower():
return 'Font issues'
else:
@ -2325,6 +2327,26 @@ class SVGQualityChecker:
"(<path>/<polygon>/<polyline>/<circle>/<image>) — verify dense content "
"pages aren't just text + boxes.")
def gate_verdict_line(self) -> Optional[str]:
"""One deterministic line summarizing the gate outcome, meant to be the
LAST stdout line on failure.
Failure-cluster tooling (core/toolfail) signatures a non-zero shell exit
by the last content line before ``[exit N]``. Without this, that line is
always the generic trailing tip ``4. Font issues: ...`` so every kind
of quality-gate failure (typography px, alignment drift, spec_lock, ...)
collapses onto one misleading "Font issues" cluster. This gives it a
stable, honest signature instead. ``None`` when there are no errors.
"""
if self.summary['errors'] <= 0:
return None
top = sorted(self.issue_types.items(), key=lambda kv: (-kv[1], kv[0]))[:3]
cats = ", ".join(k for k, _ in top) or "unspecified"
return (
f"[GATE FAIL] svg_quality_checker: {self.summary['errors']} error "
f"file(s) / {self.summary['total']} total; top issues: {cats}"
)
def print_summary(self):
"""Print check summary"""
print("=" * 80)
@ -2576,6 +2598,12 @@ def main() -> None:
output_file = sys.argv[idx + 1]
checker.export_report(output_file)
# Deterministic gate verdict as the LAST stdout line on failure (after any
# --export [REPORT] line) so core/toolfail signatures this cluster honestly.
verdict = checker.gate_verdict_line()
if verdict:
print(verdict)
# Return exit code
if checker.summary['errors'] > 0:
sys.exit(1)

View File

@ -331,5 +331,48 @@ class SpecContractTests(unittest.TestCase):
self.assertFalse(any("CJK" in e for e in r["errors"]))
class GateVerdictLineTests(unittest.TestCase):
"""gate_verdict_line() 给 core/toolfail 一条稳定诚实的失败签名,
替掉[exit] 前最后一行永远是通用 tip 全塌成 Font issues的误标"""
def _checker(self, body: str):
with TemporaryDirectory() as tmp:
page = _write_page(Path(tmp), "01_cover.svg", body)
checker = SVGQualityChecker()
checker.check_file(str(page))
return checker
def test_none_when_no_errors(self):
# 干净页面(无 error)不出 gate 行 —— exit 0 本就不进失败面板
c = self._checker(_text(60, 100, "干净标题"))
self.assertEqual(c.summary["errors"], 0)
self.assertIsNone(c.gate_verdict_line())
def test_gate_line_on_error(self):
# foreignObject 是 error;gate 行须以 [GATE FAIL] svg_quality_checker 打头,
# 带文件计数与 top issues —— toolfail 归一后是诚实签名而非 "Font issues"
c = self._checker('<foreignObject x="0" y="0" width="9" height="9"><b/></foreignObject>')
self.assertGreater(c.summary["errors"], 0)
line = c.gate_verdict_line()
self.assertIsNotNone(line)
self.assertTrue(line.startswith("[GATE FAIL] svg_quality_checker:"))
self.assertIn("top issues:", line)
self.assertIn("foreignObject", line)
def test_gate_line_is_last_stdout_line(self):
# 契约:gate 行是 print_summary 之后的最后一行 stdout(toolfail 取尾行)
import contextlib
import io
c = self._checker('<foreignObject x="0" y="0" width="9" height="9"><b/></foreignObject>')
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
c.print_summary()
line = c.gate_verdict_line()
if line:
print(line)
last = [ln for ln in buf.getvalue().splitlines() if ln.strip()][-1]
self.assertTrue(last.startswith("[GATE FAIL] svg_quality_checker:"))
if __name__ == "__main__":
unittest.main()