lili d00938ccbd feat(cheap-worker): M3b 本机 U1-U3 — 9d trace parity 救活 D11 + D9 vendored + 跨语言反假绿门(切片一 M3b)
便宜档生成切到 cheap-worker 新路后,result_out 只产 trace.cost、缺 9d trace 七项,致后端 D11 ReadinessScorer 三维恒中性、退化成 45/55 双峰近恒定分,D9 反同质失效。

U1 cheap_studio 富化:build_trace_source + _furthest_stage,additive 捕获 verdictFull/driverType/models/stage。U2 result_out._build_trace:七项(repairs=max(0,attempts-1))+ sevenGateVerdict{pass,guards} + gatespec{driver} + cost + similarity,camelCase 镜像 _extract_trace、None 容错。U3 D9 dedup vendored 进 cheap-worker/dedup.py(registry 指自己 results/)、worker_service 接线非阻断。

Java 跨语言切片测加逐维反假绿门(反射调真 ReadinessScorer:三维脱离中性 + 合成分 100 vs 退化 ≤55 拉开 ≥35)。顺带修预存 M3a 红测:worker 异常兜底 failureReason 改传真枚举 llm_error。

验证:cheap-worker 全 16 测绿 + Java 切片 5/5 绿 BUILD SUCCESS。范围仅 cheap-worker/ + 2 fixture + 1 Java 测 + plan,零 live 触碰。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 02:17:41 -07:00

123 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""dedup.py —— D9 反同质化(归一 + 精确撞重签名 + FS 登记查重)。便宜档 M3b U3。
【来源 = vendored 复制 wg1/gen-worker/worker/agent_loop/dedup.py 的纯逻辑】
norm_text / compute_sig / _load_registry / _append_registry / check_similarity 全 stdlib、无外部依赖
逐字搬运wg1 与 tier2/gen-worker 都不在 cheap-worker 的 import 路径上、tier2 也无 dedup故用 vendor 复制、
不跨树 import见 KTD2。**唯一功能改动**DEDUP_REGISTRY 指 cheap-worker 自己的 results/(天然路径隔离,
绝不与 wg1 串台);另把日志前缀 [wg1-worker] 改 [cheap-worker](可追溯,不改逻辑)。
【职责】给生成结果算「相似度」字段dupHit/titleNorm/sig/dupWithv0 只做【精确撞重 + 归一完全相等】
告警,**绝不阻断生成**九门已是硬地板挡坏游戏D9 首版只观测,避免误杀)。撞重只 log.warn +
similarity.dupHit=true照常回调、照常入 feed。
【容错】登记表损坏/读写失败 → try-catch 吞 → similarity 段降级dupHit=None不阻断生成。
"""
import hashlib
import json
import re
import unicodedata
from pathlib import Path
# 查重登记表FS 侧 append-only jsonl
# 【唯一功能改动】指 cheap-worker/results/_dedup_registry.jsonl本文件在 cheap-worker/ 根parent 即 cheap-worker/
# 天然路径隔离、跨任务持久,绝不写 wg1别用 wg1 的 parents[1])。
DEDUP_REGISTRY = Path(__file__).resolve().parent / "results" / "_dedup_registry.jsonl"
def norm_text(text):
"""查重归一化(移植 run_batch.py:96 _norm_text纯函数≈复制NFKC + casefold + 去空白与常见中英文标点。"""
s = unicodedata.normalize("NFKC", text or "").casefold()
return re.sub(
r"[\s,.;:!?'\"()\[\]{}<>~`@#$%^&*+=/\\|,。·、!?:;…“”‘’()《》【】—-]+", "", s
)
def compute_sig(title_norm, theme_norm, struct_key=""):
"""精确撞重键(移植 ledger.py:50 mint_design_id 思路sha256(titleNorm+themeNorm+关键结构) hex64。
完全相同的 title/theme/结构 → 同 sig撞重判定的精确键。纯拼接 + UTF-8 编码,可复现。
:param title_norm: 归一后标题norm_text 产)
:param theme_norm: 归一后主题norm_text 产)
:param struct_key: 关键结构串engine 路可空;填参路可放 templateId 等结构标识)
:return: 64 位小写 hex
"""
raw = "%s|%s|%s" % (title_norm, theme_norm, struct_key)
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
def _load_registry():
"""读 FS 登记表 → [{traceId, sig, titleNorm, themeNorm}](坏行跳过,文件缺失返回空)。
容错:单条坏行(崩溃残行/手改脏数据)跳过不抛;整文件读失败由调用方 try-catch 兜(返回空表 = 视为无历史)。
"""
if not DEDUP_REGISTRY.is_file():
return []
rows = []
for line in DEDUP_REGISTRY.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line:
continue
try:
rows.append(json.loads(line))
except Exception:
# 坏行跳过borrow ledger.py resume_state 的"崩溃残行容忍"范式),不阻断查重
continue
return rows
def _append_registry(entry):
"""登记本款 sig 到 FS 表append-only落盘即 flush目录不存在则建"""
DEDUP_REGISTRY.parent.mkdir(parents=True, exist_ok=True)
with DEDUP_REGISTRY.open("a", encoding="utf-8") as fh:
fh.write(json.dumps(entry, ensure_ascii=False) + "\n")
fh.flush()
def check_similarity(title, theme, trace_id, struct_key="", register=True):
"""D9 v0 查重主入口:归一 → 算签名 → 比对 FS 历史 → 命中告警 + 登记本款。
v0 范围:完全相同(归一后 sig 相等)= 撞重;不做模糊近似阈值。
:param title: 游戏标题engine 路 = brief 派生的 gameConfig.title
:param theme: 游戏主题engine 路占位 generic
:param trace_id: 本款 traceId命中时登记到 dupWith 供溯源)
:param struct_key: 关键结构串(参与 sig可空
:param register: 是否把本款登记进 FS 表(默认 True查重不命中才登记命中只告警不登记防表膨胀
:return: similarity dict {dupHit, titleNorm, sig, dupWith};读写异常时 dupHit=None降级不阻断
"""
title_norm = norm_text(title)
theme_norm = norm_text(theme)
sig = compute_sig(title_norm, theme_norm, struct_key)
try:
rows = _load_registry()
dup_with = None
for r in rows:
if r.get("traceId") == trace_id:
continue # 排除自身(重试/重投同 traceId 不与自己撞)
# 精确撞重sig 完全相等(同 title/theme/结构)即命中;归一完全相等亦体现在 sig 相等
if sig and r.get("sig") == sig:
dup_with = r.get("traceId") or r.get("sig")
break
# 归一标题完全相等(即便 struct_key 不同也告警,对齐 _dedup_gate 的 title 维度撞重)
if title_norm and r.get("titleNorm") == title_norm:
dup_with = r.get("traceId") or r.get("sig")
break
dup_hit = dup_with is not None
if dup_hit:
# v0 不阻断铁律:撞重只告警 + 标 dupHit照常回调入 feed命中不再登记防同款刷表
print(f"[cheap-worker] [D9] 撞重告警 traceId={trace_id} sig={sig[:12]} dupWith={dup_with}(仅告警不阻断)",
flush=True)
elif register:
# 不撞 → 登记本款 sig供后续款查重
_append_registry({"traceId": trace_id, "sig": sig,
"titleNorm": title_norm, "themeNorm": theme_norm})
return {"dupHit": dup_hit, "titleNorm": title_norm, "sig": sig, "dupWith": dup_with}
except Exception as e:
# 登记表损坏/读写失败 → 降级dupHit=None 表"未查重"),不阻断生成
print(f"[cheap-worker] [D9] 查重降级登记表读写异常不阻断traceId={trace_id}: {type(e).__name__}: {e}",
flush=True)
return {"dupHit": None, "titleNorm": title_norm, "sig": sig, "dupWith": None}