iter56-卡1:结算期文字列表退役与日志零文案(迭代56卡1): 撤B区逐行文字列表(row_pacer文字入带三调用/行格式化域删净——FX排播链零diff保留)+日志step行零文案(text/display_name不落盘)+reward_ineffective六发射点补reason结构化(full_layers/full_hp/no_enemy_alive/hero_down); 分析工具改读结构化字段(target==all_enemies/reason优先text兜底双分支)+analyze_parity_check对拍载体。读数: R1零逐行文字/R2 FX链行型分布独立推导逐位一致/R3零文案+全字段落盘/R4判读等价三读数全一致/R5四类reason逐一命中——各T提交信息载; 能力行:137/:138名实一致(_smoke_settle_zero_rows/_assert_fx_chain/_assert_no_text_and_full_payload/_assert_four_reasons); 测试削弱全部申报内(§10清单+§5.5同生死波及面11处登记实现报告); 中档审计通过

This commit is contained in:
DKChan committed 2026-11-08 03:47:33 +08:00
1 parent 9251d35650
commit 908e518306
40 files changed
+1258 -1298

No files matched your search

+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""判读等价对拍工具(结算期文字列表退役批 R4 载体——日志零文案改后分析判读逐位一致)。
用法:
python tools/analyze_parity_check.py <输入A> <输入B> [<输入C> ...] [--runs N]
输入 = 日志(.jsonl,各跑一次 tools/analyze_playtest.py 完整判读)或基线快照
(.md,analyze_playtest.py 输出重定向落盘的静态文本,直读不重跑)。全部剥去文件名
标题行后相邻逐位 diff——任一对不一致即 exit 1 并打印第一条分歧行。
三读数跑法(改前改后配对,卡 R4):
① 历史锚日志 vs 改前 T0 基线快照(兼容分支等价)
② 改前同种子日志 vs 改前 T0 基线快照(复现等价)
③ 改前同种子日志 vs 改后同种子新日志(同种子+玩法零改动=事件流逐位同)
退出码: 0=全部逐位一致 1=判读分歧 2=参数/执行错误
"""
import subprocess
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
ANALYZE = HERE / "analyze_playtest.py"
def run_analyze(log_path: str, runs: str) -> str:
cmd = [sys.executable, str(ANALYZE), log_path]
if runs:
cmd += [runs]
proc = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8")
if proc.returncode != 0:
print("[parity] 判读执行失败(%s)exit=%d" % (log_path, proc.returncode))
print(proc.stdout[-2000:])
print(proc.stderr[-2000:])
sys.exit(2)
# 剥文件名标题行(playtest_*.jsonl 文件名随输入变,不参与逐位比对)
return strip_name_lines(proc.stdout)
def strip_name_lines(text: str) -> str:
out_lines = []
for ln in text.splitlines():
if "playtest_" in ln and ".jsonl" in ln:
continue
out_lines.append(ln)
return "\n".join(out_lines)
def first_diff(a: str, b: str):
la, lb = a.splitlines(), b.splitlines()
for i in range(max(len(la), len(lb))):
xa = la[i] if i < len(la) else "<缺行>"
xb = lb[i] if i < len(lb) else "<缺行>"
if xa != xb:
return i + 1, xa, xb
return None
def main():
args = sys.argv[1:]
runs = ""
if "--runs" in args:
i = args.index("--runs")
runs = args[i + 1]
args = args[:i] + args[i + 2:]
if len(args) < 2:
print(__doc__)
sys.exit(2)
outputs = []
for p in args:
if p.lower().endswith(".jsonl"):
outputs.append(run_analyze(p, runs))
else: # 基线快照(.md)——静态文本直读,剥名规则同判读输出
outputs.append(strip_name_lines(Path(p).read_text(encoding="utf-8")))
ok = True
for i in range(1, len(outputs)):
d = first_diff(outputs[i - 1], outputs[i])
pair = "%s <-> %s" % (Path(args[i - 1]).name, Path(args[i]).name)
if d is None:
print("[parity] 一致 | %s | 判读逐位一致" % pair)
else:
ln, xa, xb = d
print("[parity] 分歧 | %s | 第 %d 行" % (pair, ln))
print(" A: %s" % xa)
print(" B: %s" % xb)
ok = False
sys.exit(0 if ok else 1)
if __name__ == "__main__":
main()
+23 -8
View File
@@ -186,7 +186,7 @@ def analyze_run_core(rows, meta):
if cur_b is not None:
cur_b["hero_dmg"] += v
elif src in ("safe_cell", "clear_reward", "flag_hit") or (
src == "mine_hit" and "全体敌人" in r.get("text", "")):
src == "mine_hit" and r.get("target") == "all_enemies"):
out["reward_dmg"] += v
out["rewards_n"] += 1
name = "清障炸药" if src == "clear_reward" else "群伤"
@@ -431,7 +431,7 @@ def analyze(rows, label):
cf_n = int(round(n * 100.0 / (100 + up)))
rowout["cf_hero"] += cf_n
elif src in ("safe_cell", "clear_reward", "flag_hit") or (
src == "mine_hit" and "全体敌人" in s.get("text", "")):
src == "mine_hit" and s.get("target") == "all_enemies"):
# 奖励伤害 = 打敌人的棋盘来源伤害;踩雷负向打主角的(反噬震荡)归承伤侧
rowout["rw"] += int(s["value"])
rowout["pos_n"] += 1
@@ -468,17 +468,32 @@ def analyze(rows, label):
rowout["cf_tr"] += cf_norw
for s in steps:
if s["type"] == "reward_ineffective":
txt = s.get("text", "")
if "满层" in txt:
# 结构化 reason 优先(日志零文案批——四类枚举 full_layers/full_hp/
# no_enemy_alive/hero_down);text 子串兜底 = 旧日志兼容分支(改前日志
# 无 reason 键——判读等价两腿各走各分支,结论逐位一致)
reason = s.get("reason", "")
if reason == "full_layers":
key = "增益满层"
elif "满血" in txt:
elif reason == "full_hp":
key = "回血满血"
elif "无存活敌人" in txt:
elif reason == "no_enemy_alive":
key = "无存活敌人"
elif "主角已倒下" in txt:
elif reason == "hero_down":
key = "主角已倒下"
else:
elif reason:
key = "其他"
else:
txt = s.get("text", "")
if "满层" in txt:
key = "增益满层"
elif "满血" in txt:
key = "回血满血"
elif "无存活敌人" in txt:
key = "无存活敌人"
elif "主角已倒下" in txt:
key = "主角已倒下"
else:
key = "其他"
rowout["ineff_counts"][key] = rowout["ineff_counts"].get(key, 0) + 1
bstat["hero_dmg"] += rowout["hero"]
bstat["reward_dmg"] += rowout["rw"]
+9 -21
View File
@@ -20,8 +20,6 @@ gate_game_path() {
# 杀整棵进程树(<pid>)——递归杀子进程后杀本进程。macOS/Linux 走 pgrep -P;
# Windows 侧 MSYS kill 对其可见进程有效,带 wrapper 树的场景仍由调用方原 taskkill /T 逻辑负责。
# 注意:Windows Git Bash 实测无 pgrep/pkill(2026-11-08 修复批)——本函数在 Windows 退化成只杀
# 顶格进程,引擎子进程残留由调用方段末 kill_orphans 清场兜底(probe_gate/render_gate 均有段后清点)。
gate_kill_tree() {
local p c
kill -9 "$1" 2>/dev/null
@@ -31,36 +29,26 @@ gate_kill_tree() {
# 带超时执行(coreutils timeout 的纯 bash 替代,双平台同语义):
# gate_run_timeout <秒> <日志路径> <命令...>
# 命令输出重定向到日志;退出码=命令退出码,超时=124(与 coreutils 语义一致,判据与既有闸一致)。
# 实现=后台启动+守卫轮询(守卫每 0.2 秒醒一次,看两样:停止文件在不在/到点没;
# 命令先完时父流程放停止文件,守卫看见即自己退场——没有嵌套长 sleep,从机制上无孤儿可留;
# 2026-11-08 修复批:旧版 mktemp 建标志文件「建时即真」致退出码恒 124+守卫被杀留孤儿 sleep
# 占输出管道至满档);到点未见停止文件=真超时:落标志文件+杀整树+回 124。
# 实现=后台启动+守卫落标志文件(probe_gate run_suite 同款手法:taskkill 强杀后 wait 报 0 的
# 假绿以标志文件纠偏);超时先杀整树再回 124。
gate_run_timeout() {
local t="$1" log="$2"; shift 2
local kf stop
kf="${TMPDIR:-/tmp}/gatetime.$$.$RANDOM.kf" # 只赋路径、不建文件——建时即真=旧缺陷病根(probe_gate run_suite 同款)
stop="${TMPDIR:-/tmp}/gatetime.$$.$RANDOM.stop"
rm -f "$kf" "$stop"
local kf
kf="$(mktemp "${TMPDIR:-/tmp}/gatetime_XXXXXX")"
"$@" >"$log" 2>&1 &
local pid=$!
( end=$((SECONDS+t))
until [ -f "$stop" ] || [ "$SECONDS" -ge "$end" ]; do
sleep 0.2 2>/dev/null || sleep 1
done
if [ ! -f "$stop" ]; then
touch "$kf"
gate_kill_tree "$pid"
fi
( sleep "$t" 2>/dev/null
touch "$kf"
gate_kill_tree "$pid"
true ) &
local guard=$!
wait "$pid"; local st=$?
touch "$stop" # 放停止文件=通知守卫收工,0.2 秒内自退,无需 kill
wait "$guard" 2>/dev/null
kill "$guard" 2>/dev/null; wait "$guard" 2>/dev/null
if [ -f "$kf" ]; then
gate_kill_tree "$pid"
st=124
fi
rm -f "$kf" "$stop"
rm -f "$kf"
return "$st"
}
+2 -2
View File
@@ -2,7 +2,7 @@
# 合回前置检查钩子(git pre-merge-commit,2026-11-06 用户裁)——主工作区 git merge 落合并提交前
# 跑最小验证面:tools/run_all_gates.sh --premerge(冒烟测试+单元与集成全套件 4 分片,热导入缓存约 2 分钟)。
# 语义(git 2.45.2.windows.1 本机实测):本钩子拦的是「合并提交」不是「合并动作」——exit≠0 时合并结果
# 已进暂存区、仓库停 MERGE_HEAD 待提交态;处置唯一:git merge --abort 放弃 → 回 worktree 处理 → 重新 git merge --no-ff(禁 git commit 续合:该路径 pre-merge-commit/post-merge 两钩子零触发)。
# 已进暂存区、仓库停 MERGE_HEAD 待提交态;处置二选一:修红后 git commit 续合,或 git merge --abort 放弃。
# 快进合并不触发本钩子(git 语义)——本仓合回铁规恒 --no-ff,漏写时远端 CI 兜底仍在。
# --no-verify 可整体跳过(实测)——仅属用户,agent 禁用(worktree-guard 在案)。
# 仅主工作区触发(worktree 内合并不触发,与 post-merge 同款 git-dir 判别)。
@@ -18,7 +18,7 @@ RC=$?
if [ "$RC" -ne 0 ]; then
echo "" >&2
echo "[pre-merge-commit] ✗ 前置检查红(读数如上)——合并提交未落,仓库停 MERGE_HEAD 待提交态。" >&2
echo " 处置唯一:git merge --abort 放弃 → 回 worktree 处理 → 重新 git merge --no-ff(禁 git commit 续合:两合回钩子零触发)。" >&2
echo " 处置二选一:① 修红后 git commit 续合本合并;② 放弃整次合并=git merge --abort。" >&2
exit 1
fi
echo "[pre-merge-commit] ✓ 前置检查绿——合并提交继续。"