track: 新增 OpenWork / Orca / Maka 三项目,样本达到 4 个

样本
- openwork (different-ai/openwork) 23.2k★ 能力分发层,DCO + evals + worlds
- orca (stablyai/orca) 58.8k★ 并行编排外壳,5000 issue 实为团队自开任务板
- maka (apache/maka) 4.4k★ 执行记录层,Apache 孵化但无正式 release

档案
- 三份五维档案填完,risk.claim_vs_reality 均已核实到证据
- ipollowork 象限坐标从占位 9/9/9/9 校准为 8/7/7/7(与新样本同一标尺)

文章
- 新增横评文《四个 Agent 工作台的路线分野:把「多引擎中立」拆开看》(emlog id=99)
- 总纲文回填四项目矩阵 + 双视图象限图 (emlog id=98)

镜像
- gitinbox/maka 已推送(public,source-only 快照 12 MB)
- openwork(291MB) / orca(671MB) 受 50KB/s 带宽约束,档案标 mode: pending

规则
- license-tiers.yaml 新增「目录切分许可」降级流程(OpenWork 判例:
  排除 ee/ 后为纯 MIT → public,完整镜像 → private)
- registry.yaml openwork 可见性改 public + mirror_excludes: ["ee/"]

脚本修复
- _common.py 新增 upsert_block:手工整块覆盖 ecosystem 删掉 metrics 时能自愈
- mirror_sync.sh:SOURCE_ONLY 未初始化(set -u 下偶发炸);
  浅仓推送被服务端拒绝 → 自动 --unshallow,失败降级快照;
  量体积前先 repack(此前新 commit 全是 loose object,恒打印 0 MB)
- build_matrix.py:成熟度长文本移出表格(新增 maturity_level 短标签);
  象限图标签靠右时翻转到左侧防溢出;SVG 加 max-width 防窄屏溢出;
  文章索引改为自动生成(原先手抄,横评文发了索引里却没有)
- publish_emlog.py:ID 回写按「有无同名档案」分流,
  00-* 跨项目文章落到 articles/<id>.meta.yaml(此前 ID 无处可写)
This commit is contained in:
gitea-publish
2026-09-01 19:27:21 +08:00
parent 78b87e8b2f
commit e5cdb1801e
18 changed files with 1771 additions and 114 deletions
+138 -4
View File
@@ -28,6 +28,28 @@ from _common import ARTICLES, PROJECTS, load_yaml # noqa: E402
QUAD_THRESHOLD = 4
BEGIN, END = "<!-- MATRIX:BEGIN -->", "<!-- MATRIX:END -->"
IDX_BEGIN, IDX_END = "<!-- INDEX:BEGIN -->", "<!-- INDEX:END -->"
OVERVIEW_AID = "00-overview"
def short_maturity(risk: dict) -> str:
"""成熟度在档案里是一整段判断(几十字),直接塞进表格会把列撑爆。
优先用档案给的 `maturity_level` 短标签(人工判断最准);
没有则从长文本截取第一个分句兜底。
"""
r = risk or {}
lv = r.get("maturity_level")
if lv:
return str(lv)
m = (r.get("maturity") or "").strip()
if not m:
return "-"
for sep in ("。", ";", "\n"):
if sep in m:
m = m.split(sep)[0]
break
return m[:14] + ("…" if len(m) > 14 else "")
def cell(v, empty="-") -> str:
@@ -93,9 +115,18 @@ def build_tables(items: list[dict]) -> str:
stale = " ⚠" if m.get("stale") else ""
add(f"| **{d.get('name','')}** | {cell(m.get('stars'))} | {cell(m.get('forks'))} | "
f"{cell(m.get('open_issues'))} | {cell(m.get('latest_release'))} | "
f"{cell(m.get('release_cadence'))} | {cell((d.get('risk') or {}).get('maturity'))} | "
f"{cell(m.get('release_cadence'))} | {short_maturity(d.get('risk') or {})} | "
f"{cell(m.get('data_date'))}{stale} |")
# 成熟度是长文本,塞进表格会把列撑爆。表格里只放短标签,
# 完整的成熟度判断单独列在表后,读者想看细节再往下读。
mats = [(d.get("name", ""), (d.get("risk") or {}).get("maturity")) for d in items]
if any(v for _, v in mats):
add("\n**成熟度详解**\n")
for name, v in mats:
if v:
add(f"- **{name}**:{v}")
# 宣称 vs 实测:全套矩阵里信息密度最高的一张表
rows = []
for d in items:
@@ -121,6 +152,87 @@ def build_tables(items: list[dict]) -> str:
return "\n".join(L)
# ---------------------------------------------------------------- 文章索引
def read_h1(aid: str) -> str:
"""取 articles/<aid>.md 的第一个一级标题作为文章名。"""
p = ARTICLES / f"{aid}.md"
if not p.exists():
return aid
for line in p.read_text(encoding="utf-8").lstrip().split("\n"):
if line.startswith("# "):
return line[2:].strip()
return aid
def collect_cross_articles() -> list[dict]:
"""跨项目文章(总纲 / 横评 / 专题)的发布元数据。
ID 落在 articles/<aid>.meta.yaml 里,由 publish_emlog.py 维护。
总纲文自身要排除——索引出现在总纲内部,自己链自己没有意义。
"""
out = []
for mp in sorted(ARTICLES.glob("*.meta.yaml")):
aid = mp.name[: -len(".meta.yaml")]
if aid == OVERVIEW_AID:
continue
d = load_yaml(mp) or {}
if not d.get("emlog_id"):
continue
out.append({
"aid": aid,
"title": read_h1(aid),
"emlog_id": d["emlog_id"],
"url": d.get("url") or f"https://blog.apescale.com/?post={d['emlog_id']}",
"last_published": d.get("last_published", ""),
})
return out
def article_index_section(items: list[dict]) -> str:
"""文章索引。
这段原来是手抄的,结果新发了横评文、索引里却没有它——手写的索引
每发一篇文章就会漂移一次。改成从两处事实源生成:
项目深度文 ← data/projects/*.yaml 的 article 块
跨项目文章 ← articles/*.meta.yaml
"""
L = ["<!-- INDEX:BEGIN -->"]
cross = collect_cross_articles()
L.append("| 项目 | 一句话结论 | 深度文 |")
L.append("| --- | --- | --- |")
fallback = cross[0]["url"] if cross else ""
for d in items:
art = d.get("article") or {}
url = art.get("url") or ""
if url:
link = f"[已发布]({url})"
elif fallback:
link = f"待发布(见下方横评)"
else:
link = "待发布"
one = (d.get("verdict") or {}).get("one_liner") or "-"
# 一句话结论本来就长,索引表里截断到第一个分句,避免一行几十字
for sep in ("——", "。", ";"):
if sep in one:
one = one.split(sep)[0]
break
L.append(f"| **{d.get('name','')}** | {one} | {link} |")
if cross:
L.append("\n**跨项目文章**\n")
for a in cross:
L.append(f"- [{a['title']}]({a['url']}) — {a['last_published']}")
pending = [d.get("name", "") for d in items if not (d.get("article") or {}).get("url")]
if pending:
L.append(f"\n> 待补深度文:{'、'.join(pending)}。"
f"当前可先读上方跨项目文章,其中已包含这些项目的完整分维度对比。")
L.append("<!-- INDEX:END -->")
return "\n".join(L)
# ---------------------------------------------------------------- 图
def stack_diagram(items: list[dict]) -> str:
"""项目 <4 个时的赛道分层图:按交互形态把项目归到三层栈。"""
@@ -161,8 +273,12 @@ def quadrant_svg(items: list[dict]) -> str:
svg = []
for i, (title, xk, yk, xlo, xhi, ylo, yhi) in enumerate(plots):
ox = i * (W + 30)
# max-width:100% + height:auto 不能省:SVG 带固定 width/height 属性,
# 在窄屏(手机、窄主题)里会撑破容器横向溢出。有 viewBox 时浏览器
# 才能按这两个声明等比缩放。
svg.append(f'<svg viewBox="0 0 {W} {H}" width="{W}" height="{H}" '
f'xmlns="http://www.w3.org/2000/svg" style="font-family:sans-serif">')
f'xmlns="http://www.w3.org/2000/svg" '
f'style="font-family:sans-serif;max-width:100%;height:auto">')
svg.append(f'<text x="{W//2}" y="26" text-anchor="middle" font-size="15" '
f'font-weight="600" fill="#1f2328">{title}</text>')
px, py, pw, ph = PAD, 55, W - PAD * 2, H - PAD - 55
@@ -190,7 +306,16 @@ def quadrant_svg(items: list[dict]) -> str:
name = d.get("name", "")
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"/>')
svg.append(f'<text x="{cx+11:.1f}" y="{cy+4:.1f}" font-size="12" fill="#1f2328">{name}</text>')
# 标签默认画在点右侧,但满分点会顶到绘图区右边界(cx == px+pw),
# 文字就会溢出画布被裁掉。靠右时翻到点的左侧。
# 估算文字宽度:中文按 12px、其余按 7px,够用且不依赖字体度量。
tw = sum(12 if ord(ch) > 0x2E80 else 7 for ch in name)
if cx + 11 + tw > W - 4:
svg.append(f'<text x="{cx-11:.1f}" y="{cy+4:.1f}" text-anchor="end" '
f'font-size="12" fill="#1f2328">{name}</text>')
else:
svg.append(f'<text x="{cx+11:.1f}" y="{cy+4:.1f}" font-size="12" '
f'fill="#1f2328">{name}</text>')
svg.append("</svg>")
return "\n".join(svg)
@@ -216,7 +341,7 @@ def main() -> int:
print(block)
return 0
path = ARTICLES / f"{'00-overview'}.md"
path = ARTICLES / f"{OVERVIEW_AID}.md"
if not path.exists():
print(f"总纲文不存在:{path}")
return 1
@@ -225,6 +350,15 @@ def main() -> int:
print(f"总纲文缺少 {BEGIN} / {END} 占位标记,未回填。")
return 1
text = re.sub(rf"{re.escape(BEGIN)}.*?{re.escape(END)}", lambda _: block, text, flags=re.S)
# 文章索引同样回填,两个块各管一段,互不干扰
index = article_index_section(items)
if IDX_BEGIN in text and IDX_END in text:
text = re.sub(rf"{re.escape(IDX_BEGIN)}.*?{re.escape(IDX_END)}",
lambda _: index, text, flags=re.S)
else:
print(f" [warn] 总纲文缺少 {IDX_BEGIN} / {IDX_END},文章索引未回填")
path.write_text(text, encoding="utf-8")
print(f"==> 已回填总纲文:{path}")
return 0