## 这个错的形状(本脚本最不该有的一类) ``` 行210 标签: 类: kind<>'failure'(空真)=557;真判据(permission OR key NOT LIKE %failure%) = 459 行80 NAIVE: ... where mail_id is not null and kind<>'failure' 行81 REAL : ... where mail_id is not null and (kind='permission' or relay_key not like '%failure%') ⇒ 标签**省掉了 `mail_id is not null`** ⇒ 按标签字面算得 **558 / 460**,脚本却打 **557 / 459** (差的正是那 1 行未绑定 —— 两个标签口径都收它,而两个变量都不收) ★ 本脚本存在的全部理由就是"把口径写下来"(见文件头「口径声明」节), 而标签省掉限定符 ⇒ 读的人拿这个数去对话里对账**必然对不上** ⇒ 已实际发生过一轮。 ``` ## 实测证据: 同一个数字 459 在两天指称不同集合 ``` 09-25 讨论: 459 = **loose**(pi 82c0f5a9: 349+69+25+15=458 已绑定 + 1 未绑定 = 459) 09-26 脚本: 459 = **bound**(REAL 带 mail_id is not null) ⇒ 各 +1(09-25 09:26 新增一行真实投递 723493b7)后**恰好撞上同一个数** ⇒ 所以"脚本打 459"与"讨论里说 459"看起来一致,实为两个集合 ⇒ 是对账分叉的温床 ``` ## 修法(含一处我自己先写错又改回的地方) ``` ① 标签写进限定符: `已绑定 ∧ kind<>'failure'` / `已绑定 ∧ 真判据(...)` ② **同时打印 loose 口径**(新查两个变量 NAIVE_LOOSE / REAL_LOOSE), 让"这个数是 bound、那个是 loose"在**输出里看得见**,不靠读源码才知道 ★ 我第一版图省事写成 `loose = bound + 占位` —— **撤回**: 那等式只在"占位行恰好都满足该谓词" 时成立(今天占位行 kind='permission' 所以成立),而那是**当下数据的性质、不是不变量**; 占位行将来若是 failure 形状,`+占位` 就会算错。本文件 24-27 行记的正是这一类错 ("把瞬时量当断言")⇒ 改为各查一次,不搞代数捷径。 ``` ## 边界与验证 ``` · 输出改动仅两处(标签 + 新增一行说明); rc 修前=1、修后=1(既有 FAIL 来自"占位行年龄",非本次引入) · 标签字面现可复现: 已绑定∧空真=557、已绑定∧真判据=459、loose∧空真=558、loose∧真判据=460 ✓ · bash -n 通过; **未改任何断言/阈值/判据语义** —— 只修"标签与数不一致" ```
308 lines
20 KiB
Bash
Executable File
308 lines
20 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# 复算本会话里反复被引用的那几个 `relayed_mails` 数(**只读**,不写库)。
|
||
#
|
||
# # 为什么要这条
|
||
#
|
||
# 这条链上我们为同一批数**对了好几轮账**,每次都靠人在对话里重敲查询:
|
||
#
|
||
# · "summary 是 419 还是 422" —— pi 报 422,dsh 报 419;pi 后来承认报错,
|
||
# 但**双方都无法出示当时那条查询**,只能靠"再读五次"来对齐。
|
||
# · "抑制 37 还是 62" —— dsh 报 36/62、pi 报 61/37;
|
||
# 后来发现是**口径不同**(沿完整 parent 链 vs 只沿 failure 链),
|
||
# 而两个口径**都没写在信里**,所以先吵了两轮才找到差别。
|
||
# · "类 458 怎么来的" —— 判据 `kind <> 'failure'` 是**空真**
|
||
# (kind 只有 permission/summary),真正给 458 的是 relay_key 的形状。
|
||
#
|
||
# 三次的共同点**不是"谁算错了"**,而是 —— **结论落了盘,产生结论的查询没落盘**。
|
||
# docs/API.md 里留着"458""61/37"这些数,却没有任何一条能整段重跑的命令;
|
||
# 每个复算的人(包括我自己下一轮)都要**从头重写**查询,于是必然再分叉一次。
|
||
#
|
||
# ⇒ 本脚本把那几条查询**固定成命令**:谁不同意某个数,跑它、看输出。
|
||
#
|
||
# # 断言的是**不变量与关系**,不是冻结的数
|
||
#
|
||
# 库是**活**的(`relayed_mails` 随流量增减),所以分两层:
|
||
# · **不变量 / 关系** ⇒ 断言(不随后续流量变,只在"被数的东西变了"时红)
|
||
# · **当下读数** ⇒ 只**打印**(附时刻),不当断言 —— 否则明天必然假红
|
||
# 这正是本会话学到的另一条: 把**瞬时量**当断言,等于给自己埋一个定时假警报。
|
||
#
|
||
# # 口径声明(本脚本存在的主要理由)
|
||
# · `已绑定` = mail_id IS NOT NULL;`占位` = mail_id IS NULL
|
||
# · `类` 的判据是 `relay_key` 的**形状**,不是 `kind`(kind 里没有 failure)
|
||
# · 抑制数给**两种口径**(完整 parent 链 / 只沿 failure 链),因为两者差很多
|
||
#
|
||
# 用法: deploy/recount-relay-counts.sh [--json]
|
||
# 退出码: 0 = 全部**不变量**成立;1 = 有不变量不成立
|
||
#
|
||
# # ★ 为什么叫 `recount-` 而不是 `check-`(这条是我自己踩出来的)
|
||
#
|
||
# 本仓 `deploy/check-*.sh` 是**部署门禁族**的命名约定,且由
|
||
# `client/electron/test/criteria-hygiene.test.mjs` 用
|
||
# `readdirSync(DEPLOY).filter(/^check-.*\.sh$/)` **强制**:凡叫 check-*.sh 的,
|
||
# 必须被 install.sh / redeploy-*.sh 在**可执行位置**调用("判据在但走不到"是它要拦的形状)。
|
||
#
|
||
# 我这个脚本**第一版就叫 check-relay-counts.sh** ⇒ 被那条判据收进门禁族 ⇒ **把它弄红了**。
|
||
# 而按性质它**不该被接线**,两条入口接进去都是错的:
|
||
# · 接 `install.sh` ⇒ 它读 `/opt/agentmail/data/agentmail.db`,**新机器没有这个库** ⇒
|
||
# 实测 `AGENTMAIL_DB=/nonexistent` 时 rc=1 ⇒ **每次全新安装假红**;
|
||
# · 接 `redeploy-*` ⇒ 它断言的是"库的不变量",而**库是活的**,
|
||
# 计数一变就红 ⇒ 会去**回滚一次正确的部署**。
|
||
# ⇒ 所以正确修法是**改名**(进入"按需工具"族:`prune-*` / `archive-*` / `reset-*` 同族),
|
||
# 而不是改判据、也不是硬接线。
|
||
#
|
||
# ★ 顺带暴露了那条判据的一格: 它的**自称**按性质分("宣称是政策/门禁的 check-*.sh;
|
||
# 不管 check-deploy-drift.mjs 这类**按需手动工具**"),而**机制**按**扩展名**分。
|
||
# 两者在"按需工具 + .sh 后缀"这个缝里不一致 —— 我的文件正落在缝里。
|
||
# 反方向也有(真门禁 `check-plugin-snapshot.mjs` 因 .mjs **逃过**该判据)。
|
||
# ⇒ 这是 pi 那条⑬"判据的动作与它被许诺的范围不匹配"的又一实例。
|
||
|
||
set -euo pipefail
|
||
|
||
DB="${AGENTMAIL_DB:-/opt/agentmail/data/agentmail.db}"
|
||
JSON=0
|
||
[ "${1:-}" = "--json" ] && JSON=1
|
||
|
||
if [ ! -r "$DB" ]; then
|
||
printf ' [FAIL] 读不到库: %s\n' "$DB" >&2
|
||
exit 1
|
||
fi
|
||
|
||
q() { sqlite3 "file:${DB}?mode=ro" "$1" 2>/dev/null; }
|
||
|
||
# ── 当下读数(只打印,不断言)─────────────────────────────────────────
|
||
TOTAL=$(q 'select count(*) from relayed_mails;')
|
||
BOUND=$(q 'select count(*) from relayed_mails where mail_id is not null;')
|
||
PLACE=$(q 'select count(*) from relayed_mails where mail_id is null;')
|
||
# 注意: SQLite 的 group_concat(DISTINCT x, sep) **不合法**(DISTINCT 聚合只能一个参数)
|
||
KINDS=$(q "select group_concat(k,'|') from (select distinct kind as k from relayed_mails);")
|
||
KFAIL=$(q "select count(*) from relayed_mails where kind='failure';")
|
||
KIND_BAD=$(q "select count(*) from relayed_mails where kind not in ('permission','summary');")
|
||
NAIVE=$(q "select count(*) from relayed_mails where mail_id is not null and kind<>'failure';")
|
||
REAL=$(q "select count(*) from relayed_mails where mail_id is not null and (kind='permission' or relay_key not like '%failure%');")
|
||
# ★ 2026-09-26: 这两个是上面两条的 **loose 口径**(去掉 `mail_id is not null`)。
|
||
# 为什么要单独查: 会话里对账时用的"459"/"557"是 loose,而上面两条是 bound ——
|
||
# 同一个数字在两天里指称不同集合(09-25 的 loose 459 与 09-26 的 bound 459 是巧合撞上)。
|
||
# 把两个口径**同时打进输出**,这一类"数字撞车"才在默认路径上看得见。
|
||
NAIVE_LOOSE=$(q "select count(*) from relayed_mails where kind<>'failure';")
|
||
REAL_LOOSE=$(q "select count(*) from relayed_mails where kind='permission' or relay_key not like '%failure%';")
|
||
SUBSTR_N=$(q "select count(*) from relayed_mails where mail_id is not null and relay_key like '%failure%';")
|
||
PREFIX_N=$(q "select count(*) from relayed_mails where mail_id is not null and (relay_key like 'model-failure%' or relay_key like 'service-failure%' or relay_key like 'zcode-failure%');")
|
||
HOMEAGENT=$(q "select count(*) from relayed_mails where mail_id is not null and relay_key like 'homeagent:failure:%';")
|
||
RESIDUE=$(q "select count(*) from relayed_mails where relay_key like 'no-such-session%' or relay_key like '%toolu-nohuman%';")
|
||
RESIDUE_UNBOUND=$(q "select count(*) from relayed_mails where mail_id is null and (relay_key like 'no-such-session%' or relay_key like '%toolu-nohuman%');")
|
||
# ★ 锚定不变量(不随流量变,且**有代码依据**,不是"恰好为 0"):
|
||
# permission 行的键由 `<sessionId>:<tool_use_id>` 拼(zcode hooks/permission.mjs:191),
|
||
# 而 `failure` 标记由**摘要桥**写给 summary 行(dsh-mail-bridge/src/index.ts:1240 等)。
|
||
# 两条写路径**不同** ⇒ permission 行里不该出现 failure 字样。
|
||
# ⇒ 这条是用来接住"计数关系全都没变、但语义已经错"的那类变异:
|
||
# 把一行 permission 的键改成 failure 形状时,substr 与 prefix 会同增 ⇒ 关系断言仍绿。
|
||
PERM_FAIL=$(q "select count(*) from relayed_mails where kind='permission' and relay_key like '%failure%';")
|
||
SUMMARY_FAIL=$(q "select count(*) from relayed_mails where kind='summary' and relay_key like '%failure%';")
|
||
# ★ 跨表 provenance(最强的一类:锚定到**别的表里的真实引用**,不是字符串形状):
|
||
# `kind='permission'` 的行来自 `CreatePermissionMail`,那封邮件**必然**在
|
||
# `permission_requests` 里有一行(permission.go 里同事务写)。反过来,摘要转发的邮件
|
||
# 不该在 `permission_requests` 里出现。
|
||
# ⇒ 这条能接住"只改 kind/键、计数关系全不变"的变异(⑤b):
|
||
# 那种变异会让一行变成"kind=summary 但仍有 permission_requests" ⇒ 反向断言变红。
|
||
PERM_NO_REQ=$(q "select count(*) from relayed_mails r where r.kind='permission' and r.mail_id is not null
|
||
and not exists (select 1 from permission_requests p where p.mail_id = r.mail_id);")
|
||
SUM_WITH_REQ=$(q "select count(*) from relayed_mails r where r.kind='summary' and r.mail_id is not null
|
||
and exists (select 1 from permission_requests p where p.mail_id = r.mail_id);")
|
||
|
||
# ── 抑制数(两口径,Python 因需递推上溯)─────────────────────────────
|
||
SUPP=$(python3 - "$DB" <<'PY'
|
||
import sqlite3, sys, collections
|
||
db = sys.argv[1]
|
||
c = sqlite3.connect('file:%s?mode=ro' % db, uri=True)
|
||
rows = c.execute("""select r.agent_name, r.relay_key, r.mail_id
|
||
from relayed_mails r
|
||
where r.mail_id is not null and r.relay_key like '%failure:%'""").fetchall()
|
||
par = {m: p for m, p in c.execute("select mail_id, parent_mail_id from mails")}
|
||
fail_ids = {mid for _, _, mid in rows}
|
||
|
||
def prefix(k):
|
||
i = k.find('failure')
|
||
return k[:i + len('failure')] if i >= 0 else k
|
||
|
||
def root_full(mid, cap=300):
|
||
seen, st, last = set(), 0, mid
|
||
while mid is not None and st < cap:
|
||
if mid in seen:
|
||
return 'LOOP'
|
||
seen.add(mid); last = mid; st += 1; mid = par.get(mid)
|
||
return 'CAP' if st >= cap else last
|
||
|
||
def root_fail(mid, cap=50):
|
||
seen, st, last = set(), 0, mid
|
||
while mid is not None and st < cap:
|
||
if mid in seen:
|
||
return 'LOOP'
|
||
seen.add(mid); last = mid; st += 1
|
||
nxt = par.get(mid)
|
||
if nxt is not None and nxt not in fail_ids:
|
||
break
|
||
mid = nxt
|
||
return 'CAP' if st >= cap else last
|
||
|
||
out = []
|
||
loops = []
|
||
for label, rf in (('full', root_full), ('fail', root_fail)):
|
||
g = collections.defaultdict(list)
|
||
nloop = 0
|
||
for ag, k, mid in rows:
|
||
r = rf(mid)
|
||
if r in ('LOOP', 'CAP'):
|
||
nloop += 1
|
||
g[(ag, prefix(k), r)].append(mid)
|
||
out.append('%s:%d/%d' % (label, len(g), sum(len(v) - 1 for v in g.values())))
|
||
loops.append('%s:%d' % (label, nloop))
|
||
print(' '.join(out) + ' loops ' + ' '.join(loops))
|
||
PY
|
||
)
|
||
|
||
# ★ 显式接住 LOOP/CAP 计数 —— 这正是"信号在手 ≠ 信号被读"那一条:
|
||
# 递推里**算出了** LOOP,但第一版脚本没有任何断言读它 ⇒ 造环的变异**存活**。
|
||
# (本脚本自己也犯过一次这个错,记在这里。)
|
||
SUPP_MAIN="${SUPP%% loops *}"
|
||
SUPP_LOOPS="${SUPP##* loops }"
|
||
SUPP_FULL="${SUPP_MAIN%% *}"; SUPP_FAIL="${SUPP_MAIN##* }"
|
||
S_FULL_G="${SUPP_FULL#full:}"; S_FULL_G="${S_FULL_G%%/*}"
|
||
S_FULL_S="${SUPP_FULL##*/}"
|
||
S_FAIL_G="${SUPP_FAIL#fail:}"; S_FAIL_G="${S_FAIL_G%%/*}"
|
||
S_FAIL_S="${SUPP_FAIL##*/}"
|
||
L_FULL="${SUPP_LOOPS%% *}"; L_FULL="${L_FULL#full:}"
|
||
L_FAIL="${SUPP_LOOPS##* }"; L_FAIL="${L_FAIL#fail:}"
|
||
|
||
if [ "$JSON" = "1" ]; then
|
||
printf '{"total":%s,"bound":%s,"placeholder":%s,"kinds":"%s","kind_failure":%s,' \
|
||
"$TOTAL" "$BOUND" "$PLACE" "$KINDS" "$KFAIL"
|
||
printf '"kind_unknown":%s,"class_naive":%s,"class_real":%s,' \
|
||
"$KIND_BAD" "$NAIVE" "$REAL"
|
||
printf '"substr_failure":%s,"prefix_failure":%s,"homeagent_failure":%s,' \
|
||
"$SUBSTR_N" "$PREFIX_N" "$HOMEAGENT"
|
||
printf '"residue":%s,"residue_unbound":%s,' "$RESIDUE" "$RESIDUE_UNBOUND"
|
||
printf '"perm_with_failure":%s,"summary_with_failure":%s,' "$PERM_FAIL" "$SUMMARY_FAIL"
|
||
printf '"perm_without_request":%s,"summary_with_request":%s,' "$PERM_NO_REQ" "$SUM_WITH_REQ"
|
||
printf '"loops_full":%s,"loops_fail":%s,' "$L_FULL" "$L_FAIL"
|
||
# ★ 键名里必须带 **keying**(pi 2026-09-25 实测:生成器用了 `(ag, prefix(k), r)`,
|
||
# 但序列化时把 `ag` 丢了 ⇒ 任何人引用这个数都会以为它是"仅根"口径)。
|
||
# 实测两种 keying 全局差 6 封(37/6 vs 43/10,6 组全是跨 agent 合并)⇒ 名字里不写,
|
||
# 下一个人**必重犯**。scope 同为必需:本脚本报的是**全局**;session 侧同 keying 是另一个数。
|
||
printf '"suppress_full_groups_keying_agent_root_scope_global":%s,"suppress_full":%s,' "$S_FULL_G" "$S_FULL_S"
|
||
printf '"suppress_fail_groups_keying_agent_root_scope_global":%s,"suppress_fail":%s}\n' "$S_FAIL_G" "$S_FAIL_S"
|
||
exit 0
|
||
fi
|
||
|
||
# ── 断言:**不变量与关系**(不随流量变)───────────────────────────────
|
||
fail=0
|
||
chk_true() { # chk_true <说明> <1=成立 0=不成立>
|
||
if [ "$2" = "1" ]; then
|
||
printf ' [ OK ] %s\n' "$1"
|
||
else
|
||
printf ' [FAIL] %s\n' "$1"; fail=1
|
||
fi
|
||
}
|
||
chk_eq() { [ "$2" = "$3" ] && printf ' [ OK ] %s\n' "$1" || { printf ' [FAIL] %s(实测 %s,期望 %s)\n' "$1" "$2" "$3"; fail=1; }
|
||
}
|
||
|
||
printf '\n 取数: %s(只读)\n' "$DB"
|
||
printf ' 时刻: %s\n' "$(date '+%Y-%m-%d %H:%M:%S %Z')"
|
||
printf ' ⚠️ 下列读数是**快照**(库是活的);断言只看不变量与关系。\n\n'
|
||
|
||
printf ' ── 当下读数 ──\n'
|
||
printf ' total=%s 已绑定=%s 占位=%s\n' "$TOTAL" "$BOUND" "$PLACE"
|
||
printf ' distinct kind = [%s];kind 非 permission/summary = %s;kind="failure" = %s\n' \
|
||
"$KINDS" "$KIND_BAD" "$KFAIL"
|
||
# ★★ 2026-09-26 修: 下面两个标签原先**省掉了 `mail_id is not null`**,于是标签与它打印的
|
||
# 数不是同一个谓词 —— 按标签字面算得 558/460,脚本却打 557/459(差的就是那 1 行未绑定)。
|
||
# 这是本脚本**最不该有的**一类错: 它存在的全部理由就是"把口径写下来",
|
||
# 而标签省掉限定符 ⇒ 读的人拿这个数去对话里对账,**必然对不上**(实测已发生过一轮)。
|
||
# ⇒ 现在把限定符写进标签本身(`已绑定 ∧ …`),并**顺带打印去掉限定的那个口径**,
|
||
# 让"这个数是 bound 口径、那个是 loose 口径"在输出里**看得见**,
|
||
# 而不是靠读脚本源码才知道。
|
||
# ⚠️ loose 值**必须各查一次**,不能写 `bound + 占位` —— 那等式只在"占位行恰好都满足该谓词"
|
||
# 时成立(占位行今天 kind='permission' 所以两条都成立,但那是**当下数据的性质**,
|
||
# 不是不变量;占位行将来若是 failure 形状,`+占位` 就会算错)。本文件第 24-27 行
|
||
# 记的正是"把当下性质当不变量"这一类错。
|
||
printf ' 类: 已绑定 ∧ kind<>'"'"'failure'"'"'(**空真**)=%s;已绑定 ∧ 真判据(permission OR key NOT LIKE %%failure%%) = %s\n' \
|
||
"$NAIVE" "$REAL"
|
||
printf ' ★ 上面两数都是**已绑定**口径;**去掉该限定**则分别为 %s / %s(差 %s = 那批占位行里满足该谓词者)\n' \
|
||
"$NAIVE_LOOSE" "$REAL_LOOSE" "$((REAL_LOOSE - REAL))"
|
||
printf ' failure 边界: %%failure%% 子串=%s;三前缀(无 homeagent)=%s;homeagent:failure: =%s(差 %s)\n' \
|
||
"$SUBSTR_N" "$PREFIX_N" "$HOMEAGENT" "$((SUBSTR_N - PREFIX_N))"
|
||
printf ' 测试残留=%s(其中未绑定=%s)\n' "$RESIDUE" "$RESIDUE_UNBOUND"
|
||
printf ' 抑制数 [keying=(agent,根) scope=全局]: 完整 parent 链=%s 组/%s 封;只沿 failure 链=%s 组/%s 封\n' \
|
||
"$S_FULL_G" "$S_FULL_S" "$S_FAIL_G" "$S_FAIL_S"
|
||
printf ' 根计算触到 LOOP/CAP 的次数: 完整链=%s;failure 链=%s\n' "$L_FULL" "$L_FAIL"
|
||
|
||
printf '\n ── 不变量(断言)──\n'
|
||
chk_true "total == 已绑定 + 占位(两分穷尽)" "$([ "$TOTAL" -eq "$((BOUND + PLACE))" ] && echo 1 || echo 0)"
|
||
chk_true "kind 只取 permission/summary(写路径构造决定)" "$([ "$KIND_BAD" -eq 0 ] && echo 1 || echo 0)"
|
||
chk_eq "kind='failure' 的行数恒为 0" "$KFAIL" "0"
|
||
chk_true "**空真**: kind<>'failure' 的计数 == 已绑定(对全表恒真)" \
|
||
"$([ "$NAIVE" -eq "$BOUND" ] && echo 1 || echo 0)"
|
||
chk_true "真判据 **严格小于** 空真判据(否则判据没起作用)" \
|
||
"$([ "$REAL" -lt "$NAIVE" ] && echo 1 || echo 0)"
|
||
# ★ 量的是**包含关系**,不是"恰好只有四族":
|
||
# 三个前缀字面量本身都含 "failure" ⇒ 它们匹配的键必然也被 `%failure%` 匹配(子集)。
|
||
# 而 homeagent 族既不以那三者为前缀、又含 failure ⇒ 必落在**差集**里(子集)。
|
||
# 两条都由机制决定。先前写成 `HOMEAGENT == SUBSTR_N - PREFIX_N`,是把
|
||
# "当下只存在这四个族"当成了不变量 —— 任一桥改用第五种拼法(如 `pi-failure:`)
|
||
# 会让差集 +1 而 homeagent 不变 ⇒ **假红**(新增命名是正常演进,不是故障)。已实测复现。
|
||
chk_true "三前缀 ⊆ 子串(三前缀字面量都含 failure)" \
|
||
"$([ "$PREFIX_N" -le "$SUBSTR_N" ] && echo 1 || echo 0)"
|
||
chk_true "homeagent 族 ⊆ 差集(它非三前缀之一、且含 failure)" \
|
||
"$([ "$HOMEAGENT" -le "$((SUBSTR_N - PREFIX_N))" ] && echo 1 || echo 0)"
|
||
# ★ 那两行残留是**快照,不是不变量** —— 与文件头 24-27 行同一条规则。
|
||
# 未绑定那行是"claim 后早退没退键"留下的化石;**清掉它是正确动作**,
|
||
# 而 `RESIDUE == 2 && UNBOUND == 1` 会把那个正确动作判成失败
|
||
# (实测:清后 1/0 ⇒ 假红)。⇒ 改为只打印(见上面"当下读数"),
|
||
# 另用一条**与清理无关**的关系接住:未绑定必是占位,口径一致。
|
||
chk_true "残留中未绑定的 ⊆ 占位行(未绑定必是占位,两者口径一致)" \
|
||
"$([ "$RESIDUE_UNBOUND" -le "$PLACE" ] && echo 1 || echo 0)"
|
||
chk_true "两口径抑制数**不同**(沿链范围不同 ⇒ 量的是不同集合)" \
|
||
"$([ "$S_FULL_S" -ne "$S_FAIL_S" ] && echo 1 || echo 0)"
|
||
chk_true "完整链口径的抑制数 **大于** failure 链口径" \
|
||
"$([ "$S_FULL_S" -gt "$S_FAIL_S" ] && echo 1 || echo 0)"
|
||
# ★ 下面两条是补的"信号被读":递推里算了 LOOP/CAP,第一版没人读它 ⇒ 造环变异存活。
|
||
# 断言"没有触到上限"= 断言"根计算是可信的"。库真有环时它会红 —— 那正是我们要知道的。
|
||
chk_eq "完整 parent 链的根计算**未触上限**(无环/无超深)" "$L_FULL" "0"
|
||
chk_eq "failure 链的根计算**未触上限**" "$L_FAIL" "0"
|
||
# ★ 变异③的补丁: 占位行数本身就是"幂等键是否泄漏"的读数据 —— 光有 total=bound+place 不够
|
||
# (③ 把一行置 NULL,两边同时变,恒等式仍成立 ⇒ 存活)。
|
||
#
|
||
# ⚠️ 但**不能断言 `占位行 <= 1`**(pi 2026-09-25 实测,我复跑确认):
|
||
# `ClaimRelay`(INSERT 占位)与 `BindRelayMail`(UPDATE 绑定)**不在一个事务里**,
|
||
# 所以"同时有 2 个请求在飞"的正常形态就是 2 个占位行(t1 claim、t2 claim、t1 bind、t2 bind)。
|
||
# 实测(/tmp/pl/t.db:清掉化石 + 插 2 个合法在飞占位):
|
||
# PLACE=2 ⇒ 旧判据 rc=1,报 [FAIL] 占位行 <= 1 —— **把健康并发判成泄漏**
|
||
# ⇒ 阈值 1 是"只有 1 个请求在飞的那天"的快照,与它要防的病(泄漏)**不正交**。
|
||
#
|
||
# ⇒ 与泄漏正交的判别量是**行的年龄**:正常占位行只活"一个请求的时长",泄漏行活到**下一次请求之后**。
|
||
# 年龄与并发度无关(并发再大,每行寿命仍是一轮请求),也与"此刻是否恰好有请求"无关。
|
||
# 阈值取 10 分钟:远大于任何单个请求(毫秒~秒级,且 HTTP 有超时),
|
||
# 又远小于真实泄漏的尺度(那 1 行活了 12.7 天)。
|
||
# ★ 断言的是**上界**而不是"是不是 0"—— 因为 0 是"恰好没请求在飞",不是健康的条件。
|
||
IDLE_PLACE=$(q "select count(*) from relayed_mails where mail_id is null and (julianday('now') - julianday(created_at)) * 1440 > 10;")
|
||
IDLE_AGE=$(q "select cast(max((julianday('now') - julianday(created_at)) * 1440) as int) from relayed_mails where mail_id is null;")
|
||
printf ' 占位行年龄: 最老 = %s 分钟;其中 >10 分钟(疑似泄漏)= %s\n' "${IDLE_AGE:-0}" "$IDLE_PLACE"
|
||
# ★ 只打印不判红当且仅当**没有任何占位行**:那种情况下 IDLE_AGE 为 NULL/空,
|
||
# "没有样本"不等于"样本健康",不能拿它当绿(本文件反复记的假绿形状)。
|
||
chk_true "没有**超过 10 分钟**的占位行(年龄与并发度正交;阈值 1 会把 2 个在飞判成泄漏)" \
|
||
"$([ "$IDLE_PLACE" -eq 0 ] && echo 1 || echo 0)"
|
||
# ★ 变异⑤的补丁: 上面那些都是**关系**断言(A=B、A<B),而把一行 permission 的键改成
|
||
# failure 形状时,substr 与 prefix **同增** ⇒ 所有关系仍成立 ⇒ ⑤ 存活。
|
||
# 关系断言测不出"**语义**被搬动",只有**锚定到写路径**的不变量能测。
|
||
chk_eq "permission 行不含 failure 字样(写路径: 键=session:tool_use_id)" "$PERM_FAIL" "0"
|
||
# ★ 跨表 provenance 断言(接住 ⑤b:改了 kind 但计数关系全不变)
|
||
chk_eq "kind=permission 的邮件**都**有 permission_requests 行" "$PERM_NO_REQ" "0"
|
||
chk_eq "kind=summary 的邮件**都不**在 permission_requests 里" "$SUM_WITH_REQ" "0"
|
||
|
||
printf '\n'
|
||
if [ "$fail" = "0" ]; then
|
||
printf ' relayed_mails 口径复算:全部**不变量**成立\n'
|
||
exit 0
|
||
fi
|
||
printf ' relayed_mails 口径复算:**有不变量不成立** —— 被数的东西变了,或库换了\n'
|
||
exit 1
|