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/dupWith),v0 只做【精确撞重 + 归一完全相等】
告警,**绝不阻断生成**(九门已是硬地板挡坏游戏;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}