track: 新增 OpenClaw / QwenPaw / Tutti,样本达到 7 个

新增项目档案(五维 + claim_vs_reality):
- openclaw.yaml  388k★,GitHub 星标最多的仓库;个人助理赛道
- qwenpaw.yaml   34.7k★,OpenClaw 正面竞品;安全默认值最完整
- tutti.yaml     3.6k★,唯一做「多人 × 多 Agent」协作;技术维度基于本地源码实测

两处对既有结论的修正:
- OpenClaw 许可证自动定级为 L3/private 系误判。SPDX=NOASSERTION 是因为
  LICENSE 末尾多了一段第三方声明指引,原文主体是完整 MIT → 改判 L1/public。
  这是本系列第二个「SPDX 不可信」判例(第一个是 OpenWork 的 ee/ 目录切分许可)。
- OpenWork 巴士系数去重仅 3 人,与上一轮「工程治理最好」并不矛盾:
  流程规范与人员分散是两个独立维度,已在文章中区分。

脚本:
- build_matrix.py 象限图新增重合点沿小圆散开(SPREAD=11)+ 钳制 + 脚注。
  样本到 7 个时首次出现两组完全重合坐标,后画的点会整个盖住先画的,
  读者会以为少收录了一个项目——这种静默丢信息比图难看严重得多。
- build_matrix.py 跨项目文章索引改为按 emlog_id 升序(= 阅读顺序)。
  原先靠文件名 sorted() 排是碰运气:'-'(0x2D) < '.'(0x2E) 只是巧合。

发布:横评文第二弹 id=100,总纲 id=98 已更新。
This commit is contained in:
gitea-publish
2026-09-01 20:13:55 +08:00
parent 5b6e695a9d
commit ed1da08f42
11 changed files with 1286 additions and 11 deletions
+44 -4
View File
@@ -19,6 +19,7 @@
from __future__ import annotations
import argparse
import math
import re
import sys
from pathlib import Path
@@ -27,6 +28,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent))
from _common import ARTICLES, PROJECTS, load_yaml # noqa: E402
QUAD_THRESHOLD = 4
SPREAD = 11 # 重合点沿小圆散开的半径(px),需大于点的直径(14)的一半才看得出分离
BEGIN, END = "<!-- MATRIX:BEGIN -->", "<!-- MATRIX:END -->"
IDX_BEGIN, IDX_END = "<!-- INDEX:BEGIN -->", "<!-- INDEX:END -->"
OVERVIEW_AID = "00-overview"
@@ -185,6 +187,12 @@ def collect_cross_articles() -> list[dict]:
"url": d.get("url") or f"https://blog.apescale.com/?post={d['emlog_id']}",
"last_published": d.get("last_published", ""),
})
# 按 emlog_id 升序排 = 按发布先后排 = 阅读顺序。
# 这一步不能省:靠文件名 sorted() 排是碰运气——"00-comparison-2.meta.yaml"
# 恰好排在 "00-comparison.meta.yaml" 前面只是因为 '-'(0x2D) < '.'(0x2E),
# 换个命名(如 10-comparison)顺序就反了。系列文章要按发出来的先后读,
# 不能让 ASCII 码表来决定。
out.sort(key=lambda a: a["emlog_id"])
return out
@@ -295,17 +303,43 @@ def quadrant_svg(items: list[dict]) -> str:
svg.append(f'<text x="{px+pw}" y="{py+ph+22}" text-anchor="end" font-size="11" fill="#6a737d">{xhi}</text>')
svg.append(f'<text x="{px-8}" y="{py+ph+6}" text-anchor="end" font-size="11" fill="#6a737d">{ylo}</text>')
svg.append(f'<text x="{px-8}" y="{py+12}" text-anchor="end" font-size="11" fill="#6a737d">{yhi}</text>')
# 点
# ---- 重合点处理 ----
# 样本一多,必然出现坐标完全相同的项目。本体系加到第 7 个样本时首次撞上:
# 视图 A 里 Maka 与 OpenClaw 都是 (6,1),视图 B 里 iPolloWork 与 Tutti 都是 (7,7)。
# 完全重合的点会被后画的那个整个盖住——读者会以为少收录了一个项目,
# 这种"静默丢信息"比图难看严重得多,所以必须在这里兜住。
# 做法:把重合组沿一个小圆均匀散开,并在图下加一行说明交代清楚。
pts = []
for d in items:
c = d.get("chart") or {}
xv, yv = c.get(xk), c.get(yk)
if xv is None or yv is None:
continue
cx = px + pw * max(0, min(10, float(xv))) / 10
cy = py + ph * (1 - max(0, min(10, float(yv))) / 10)
pts.append({"d": d, "xv": float(xv), "yv": float(yv)})
groups: dict[tuple, list] = {}
for p in pts:
groups.setdefault((p["xv"], p["yv"]), []).append(p)
overlapped = [g for g in groups.values() if len(g) > 1]
for g in overlapped:
n = len(g)
for i, p in enumerate(g):
ang = 2 * math.pi * i / n
p["dx"] = SPREAD * math.cos(ang)
p["dy"] = SPREAD * math.sin(ang)
# 点
for p in pts:
d = p["d"]
cx = px + pw * max(0, min(10, p["xv"])) / 10 + p.get("dx", 0.0)
cy = py + ph * (1 - max(0, min(10, p["yv"])) / 10) + p.get("dy", 0.0)
# 散开后别顶出绘图区
cx = max(px + 4, min(px + pw - 4, cx))
cy = max(py + 4, min(py + ph - 4, cy))
name = d.get("name", "")
# 重合组用更高的不透明度,让读者一眼看出"这里挤了不止一个"
op = "0.9" if len(groups[(p["xv"], p["yv"])]) > 1 else "0.75"
svg.append(f'<circle cx="{cx:.1f}" cy="{cy:.1f}" r="7" fill="#0969da" '
f'fill-opacity="0.75" stroke="#fff" stroke-width="1.5"/>')
f'fill-opacity="{op}" stroke="#fff" stroke-width="1.5"/>')
# 标签默认画在点右侧,但满分点会顶到绘图区右边界(cx == px+pw),
# 文字就会溢出画布被裁掉。靠右时翻到点的左侧。
# 估算文字宽度:中文按 12px、其余按 7px,够用且不依赖字体度量。
@@ -316,6 +350,12 @@ def quadrant_svg(items: list[dict]) -> str:
else:
svg.append(f'<text x="{cx+11:.1f}" y="{cy+4:.1f}" font-size="12" '
f'fill="#1f2328">{name}</text>')
if overlapped:
# 如实交代散开这件事,别让读者以为点的位置就是精确坐标
who = ";".join("、".join(x["d"].get("name", "") for x in g) for g in overlapped)
svg.append(f'<text x="{px}" y="{H-6}" font-size="10" fill="#8b949e">'
f'※ 坐标完全重合的项目已沿小圆散开以便区分:{who}</text>')
svg.append("</svg>")
return "\n".join(svg)