Commit Graph

257 Commits

Author SHA1 Message Date
ThearchyHelios a122b98dd3 合入期 3 双盲复算与核查设置(前端):双盲审批卡(并排对照、差异说明、三选一与写目录预告)、检查器与节点卡片钩子、设置页「核查」一节、时间线解读双盲和反方审稿的日志 2026-10-05 18:08:39 +08:00
ThearchyHelios e9cf74748f 合入期 3 反方审稿(后端):报告里的关键结论各派只读 agent 主动找反例,给出经受住、被削弱、被推翻、无法判断和有出处的理由;被推翻的计入出具缺口;审批卡附反方审稿概况
# Conflicts:
#	backend/tests/test_issuance_upstream_gaps.py
2026-10-05 18:08:39 +08:00
ThearchyHelios bee24cf79a 合入期 3 双盲复算(后端):口径卡上来自查询的输入另起看不到原 SQL 的只读 agent 独立复算,代回原式按误差比对;不一致时挂起交审核人三选一,选择只在差一个方面、且是排除无效记录这类数据事实时写进数据目录;发起时快照核查设置 2026-10-05 18:08:03 +08:00
ThearchyHelios bcad199e0f 双盲复算前端复核修正(第 3 轮):同一次裁决里几个指标落到数据目录的同一项上时,卡上按其余指标当前的选择说——说法不同的改说都不记,有效记录条件说合成一句
问题:服务端写目录前把同一次裁决里落到同一张表同一项上的说法归拢(dual_check_facts._merged):业务日期、去重规则只要说法不同就一项都不写,有效记录条件用 AND 合成一句写。审批卡上的预告是每个指标单独算的,卡上不看别的指标选了什么:一个指标采用原流程(paid_at)、另一个采用盲算(created_at),两块里各自加粗说「记为……」,服务端一项都不写;两个指标都采用去重的一边、去重的列不同,也是这样。结尾只说按发起审批时的数据目录预告,看不出选择的组合也会让写入作废。审核人可能为了不让同一张表出现两个业务日期,把本该分开的选择改成一致。

审批卡(run/DualCheckApproval.tsx)
- 按其余指标当前的选择,给每个指标的两种选择各找出落到同一数据源、同一张表、同一项上的指标(peersOf)。只看预告会写的一边:选「两边都不对」、还没选的,服务端不会并进来。说法照服务端的 _same_fact 比:业务日期只看哪一列,其余压掉多余的空白比。
- 业务日期、去重规则说法不同时,那一句改说「……与「退款率」的选择说法不同,都不记」,不再算作会写。有效记录条件在「记为……」后面补一句「与「退款率」采用的条件用 AND 合成一句」。
- 选择一变,各块的说法跟着变。没选中的那几句也按别的指标当前的选择说,审核人改选之前就看得出这一选会不会让整项作废。
- 结尾改成「写入的项记为人工确认的事实;以上按发起审批时的数据目录和各指标当前的选择预告……」。原来是「两边都不对时不记。记为人工确认的事实」,读起来像在说不记的那一项。
- 没有预告时的结尾同样改。时间列、去重两句补上「同一次裁决里几个指标给同一张表记的业务日期(去重规则)不同时都不记」。

夹具与检查
- 夹具加 peers 和 merges。peers 换掉「退款率」对不上的那个输入,和「销售额」落到 orders 的业务日期、去重规则、有效记录条件上。merges 是四种组合:业务日期分开选、选同一列、去重的列不同、有效记录条件不同。
- peers 的差异说明和预告是服务端的真实输出。merges 的 written 是 record_choices 在空目录上真写一遍的结果。
- test_dual_check_fixture.py 新增两项。一项不依赖服务端,查每种组合真的落到同一项上。另一项在服务端合入后建临时数据源真写一遍,逐项比对。
- 在 vs/f1a 的 f7d1a2e 和 233afd1 上都跑过:8 项全过,regenerate 逐字节不变。
- 预览用例 dual-check-approval 认 peer 参数。
- check-verify 新增一节:
  - 分开选时,两边加粗的那一句都说都不记,写明哪张表的哪一项、和哪个指标说法不同。
  - 没选中的那一句按别的指标当前的选择说;一个指标改选两边都不对后,另一个回到「记为」。
  - 选同一列照常记;去重的列不同都不记;有效记录条件说合成一句。
  - 390px 下深浅色都不横向溢出。
- check-verify 没有预告的一节补两项:「人工确认」说的是写入的项;说法不同时都不记。
- 变红验证:去掉实现(peersOf 返回空),check-verify 变红 6 项;把有效记录条件也当成说法不同,变红 1 项;把还没选、两边都不对也当成采用了一边,变红 2 项。
- 改动后 297 项全部通过(原来 274 项)。check-all 里 check-verify 的耗时估计按实测改成 71 秒。
2026-10-05 17:47:21 +08:00
ThearchyHelios 233afd16ee 双盲复算复核整改(第 3 轮):驳回过的有效记录条件不再借着同一次裁决里的合并写回,合起来写时已有的段只写一次,补改名后分组的遮罩列用例
- 同一次裁决里几个指标落到有效记录条件上、要合成一句时,驳回检查原来只拿合起来的整句和驳回的说法比:「status <> 'refunded'」和「is_test = 0」AND 在一起就不再等于驳回的那句,被写成人工确认,和审批卡上「不写」的预告正相反。现在合并前按每个指标自己的说法过一遍驳回检查(_screen):带回驳回说法的指标拿出来不写,原因和它的预告是同一句话,其余的照写,备注只列真写进去的指标;合起来以后又包含了驳回的整句的也不写。有效记录条件的驳回检查改按段比,!= 和 <>、列名大小写、先后不同都认得出。
- 合起来的条件再和目录里已有的条件合写时,原来拿并好的整句去接,和已有条件部分重叠的整句原样接上,写出「status <> 'refunded' AND status <> 'refunded' AND is_test = 0」。现在接的是每个指标各自的条件,_and_join 只接还没出现过的段;已有条件是人工确认过的,原因里只说另外多出的那几段。最外层是 OR 的条件只在接了别的条件时加括号。
- Fact.also(其余几个指标)改成 members(并进来的每一个,各带自己的说法):并好的整句不再顶掉第一个指标自己的说法。
- 补用例:合并时驳回的条件拿出来、其余照写(facts 级和图级,真写和预告一字不差);驳回的整句由几个指标拼回来时不写;驳回时写法不同也认得出;合写不重复、已确认时只说多出的段;_and_join 的段级去重和括号;遮罩列改了名再分组(region AS area)时不收窄。
2026-10-05 17:23:17 +08:00
ThearchyHelios b206665856 双盲复算前端复核修正(第 2 轮):「记入数据目录」不再说得比服务端宽——有服务端预告时照预告说,没有时只说必要条件、不写的几种照实说
问题:审批卡上那句「记入数据目录」的提示是照 vs/f1a 早先的规则写的。F1a 在 1491b5c 把写目录的规则收紧以后,有三种常见情况卡上说会记入,服务端却都不写:
- 只有一边按时间筛:卡上写「两边只差在按哪一列筛时间」,和正上方那条差异「盲算没有按时间筛选」自相矛盾
- 只多一个指标自己范围的条件(channel = 'web'):服务端只把排除退款、作废、测试这类无效记录的条件记成有效记录条件
- 只差去不去重,而审核人采用的是不去重的一边:服务端不写
审核人可能为了不让目录被写错,避开本来正确的原流程,结果出具降档;节点设为「判为失败」时运行还会终止。

审批卡(run/DualCheckApproval.tsx)
- 服务端在摘要里另带预告(payload.summary[].catalog,F1a 在 f7d1a2e 加的,约定之外另带)时照它说:三个选择各说一句,会写的写明哪张表的哪一项记成什么,不写的写「不记」。两边都不写就不提数据目录。审核人当前选中的那一句加重,选完一眼就能看出这一选会写什么。预告跟着摘要一起来,完整对照还没到或取不到时也照样能说。预告的形状认不出时整份不用,回到有条件的说法。
- 没有预告时(老服务端、升级前挂起的审批),三句改成只说必要条件(「仅当……才」),并把不写的几种照实写出来:只有一边按时间筛时不记;多出的条件得只在一边、并且是在排除无效记录,只看某个地区、某个渠道这类条件不记;采用不去重的一边时不记。
- payload 带了实际生效的「未采用原流程时」(同样是 f7d1a2e 加的)就照它说,不再去取图快照。子工作流里的节点在图快照里找不到,有了它也能说准;值认不出时仍回到图快照。

夹具与检查
- 夹具新增两个变体:只有一边按时间筛、只多一个渠道条件,差异说明都是 explain_sql_diff 的真实输出。每个指标、每个变体都附上服务端预告的真实输出(catalog_preview、variants[].catalog)。backend/tests/test_dual_check_fixture.py 有两项检查:一项不依赖服务端,查预告的形状和写目录的门槛;另一项在服务端模块合入后,逐项比对夹具和 dual_check_facts.preview。regenerate 也会一并重新生成预告。
- 预览用例 dual-check-approval 认地址参数 variant / catalog / on_reject,不带参数时 payload 只有约定里的几项。
- check-verify 按服务端的预告核对两种说法:没有预告的一节,在三种情况下都要以「仅当」开头,并照实说出不写;只差去不去重时,两种选择各断言一次。有预告的一节,核对两边写什么或不记、两边都不写时不提、选中那一句加重(去重的两种选择分别看)、认不出时回到有条件的说法、取不到完整对照时照样说,以及 390px 下深浅色的对比度。payload 的 on_reject 那一节核对:照它说、不取图快照、子工作流也说准、认不出时回到图快照。
- 去掉实现以后,检查脚本在这三种情况、预告和 on_reject 上共有 13 项变红。改动后为 274 项全部通过(原来 218 项)。check-all 里 check-verify 的耗时估计按实测改成 65 秒。
2026-10-05 16:56:05 +08:00
ThearchyHelios 0cd96fe327 合入运行对比加固:一个维度值占几行时按维度值数两边各引用了几行,部分行挤出引用的行段、单格换了同一维度的另一行都判说不清;维度列设了遮罩时照样核对、说明只写行号;补齐循环回边、写入者措辞、严格拆分范围、单边遮罩等守护用例 2026-10-05 16:44:22 +08:00
ThearchyHelios f7d1a2ecba 双盲复算复核整改(第 2 轮):遮罩的分组列不收窄,收窄时去掉 HAVING,审批卡带上处置和写目录的预告,同一次裁决里的有效记录条件合起来写
节点(nodes/dual_check.py)
- 差异说明收窄到这一格时,分组键的结果列或它用到的列设了遮罩,就不收窄、照整条比。原流程、盲算各按自己那份快照
  的遮罩(查询当时记下的加数据源现在设的)判断。以前按这一行的分组值补等值条件,「region = '华东'」这样的遮罩列
  取值会跟着差异说明进审批 payload、human.requested 事件、对照工件和 dual_check_catalog 日志
- 按行收窄时连 HAVING 一起去掉:这一行既然在结果里,它所在的组就满足 HAVING。留着会多出一条「分组后的筛选」,
  还会占掉「只差一个方面」的名额,真正的差异(比如时间列)就写不进目录
- 审批 payload 在约定的几项之外带上实际生效的 on_reject。摘要里每个指标另带 catalog:选 a、选 b 时,按发起审批
  时的目录各会往数据目录里写什么({write: true, source, table, item, value}),不写的给出原因({write: false,
  reason})。审核人要看得出不采用原流程会不会让运行失败、某个选择会不会被记成人工确认的目录项
  (on_reject 以前只能从图快照里读,子工作流里的节点读不到)

写数据目录(dual_check_facts.py)
- 写入的判断拆成不读库的 _plan:真写(_write)和审批卡上的预告(preview)用同一套判断,不会说一套写一套
- 同一次裁决里落到同一项上的几个说法合成一项(_merged):
  - 说法一样的只写一次,备注列出每个指标
  - 有效记录条件用 AND 合成一句。derive 只收排除无效记录的条件,「排除退款」和「排除测试单」并不矛盾,以前两条
    都被丢掉,原因还写成「互相矛盾」
  - 业务日期、去重规则说法不同的仍然都不写,原因改为「给出了不同的说法」
- 合并后再和目录里已有的推断条件合起来写时,要求每个被采纳的一边都按已有条件筛过;有一个没按它筛,就不动并说明
  是哪个指标

测试
- 遮罩分组列(原流程侧、盲算侧)的取值不出现在 payload、对照工件、节点产出和节点事件里
- HAVING 收窄后只剩时间列一处差异,并写进目录
- payload 的 on_reject、摘要里的预告(一致、已确认不覆盖、指标自己的范围几种情形)
- 有效记录条件合并(含和已有推断条件合写、有一个被采纳的一边没用已有条件时不动),同一说法只写一次
- 补上复核指出的三处接线缺口,各有一条图级用例:
  - 带取舍的列名(gmv_by_order_date、paid_orders)不交给盲算
  - 取值在运行输入里的排除条件不写目录
  - 分组列上的 is_test = 0 不写目录
- 13 处变异(遮罩判断两侧、HAVING、上面三处接线、合并、预告、on_reject 等)去掉后,相关测试都会变红
2026-10-05 16:43:14 +08:00
ThearchyHelios 5f48f8727e 运行对比加固(收尾):区域等维度列设了遮罩时照样拿它核对两次引用的是不是同一批行,说明只写行号、不写值,挤出引用行段的和真的少了分得清,没挪的照常是数据变化;补单格反方向、归因维度按两边各自引用的行认、写入者改了配置时不严格拆分、并行支路查询四处守护用例 2026-10-05 16:41:59 +08:00
ThearchyHelios 52baa0d65c 双盲复算前端复核修正(第 1 轮):高亮按服务端真实的差异输出做,「记入数据目录」只在服务端真会写时有条件地说,后果和提示按节点的处置说
审批卡
- 对照两条 SQL:text_diff 是规范化之后的行(列名带表名、条件并成一行),和原文逐行对不上,原来按「两边逐行相等」高亮在真实输出上一处都标不出。改成两段原文按行做一次 LCS(不看空白、大小写和行尾逗号分号)高亮不同的行,一行都对不上时不乱标;text_diff 分原流程、盲算两栏,每行高亮(窄栏上下排)。
- 「只差一个方面、会记入数据目录」:照服务端 dual_check_facts 的门槛收紧——不等价、没用兜底、差异恰好一条(几个输入都对不上时合并后至少两条,所以也就是只有一个输入对不上),且是时间列、筛选条件或聚合;按这三种分别有条件地说会记成哪一项,选「两边都不对」不记、已有人工确认的不覆盖、以时间线为准。时间范围、两条筛选、关联、解析失败、多个输入合并都不再说。
- 后果和提交后的提示:payload 不带 on_reject,从这次运行的图快照里按节点 id 读「未采用原流程时」;判为失败时写明采用盲算或都不对会终止运行、提示也照实说,缺省时不再挂那句,读不到时有条件地说、提示不下结论。
- 只有解析失败时和摘要阶段一样按兜底说(固定那句 + 解析的原话),默认展开对照;兜底的原话另有原因(比较超时)时附在下面,一边没有 SQL 时不说「两条 SQL 的写法不同」。
- 认不出任何指标时留一个「驳回并终止」(approved 为假),运行不再一直挂着。

时间线
- dual_check_catalog 的 item 是目录项的键(business_date 等),按数据目录的叫法写成「「orders」(shop)的业务日期」,认不出的键不显示。
- dual_check_limit 的 metric 是指标 ID,不再当名字加引号。
- challenge_skipped 的 no_report 是报告文档读不出来(报告节点找不到时节点直接报错),说法和下一步改成去查报告节点有没有执行成功、在不在执行到的分支上。

检查器
- 字段名和服务端校验报错的叫法(schema.py 的 DUAL_CHECK_FIELD_LABEL)对齐,处置选项以服务端的短说法开头;check-verify 在服务端合入后逐项比对。
- 盲算模型移出高级选项:时间线「模型相同」的定位要找得到它。
- 上游有几张口径卡时写明必须选(服务端校验会挡住发起运行),不再说「建议指定」。
- 「跟随设置」的当前值每次打开检查器都重取,改了设置回到画布不再是旧值。

测试
- 夹具里的差异说明换成 explain_sql_diff 对同一对 SQL 的真实输出,新增几对 SQL 的变体;backend/tests/test_dual_check_fixture.py 核对夹具和真实输出一致,并可重新生成。
- check-verify 补真实形状下的高亮、各种差异下「记入数据目录」说不说、只有解析失败和兜底原因、按处置说后果和提示、认不出指标时的驳回、叫法比对、多张口径卡、设置重取;check-decode 改用服务端真实的日志形状。check-verify 的耗时估计按实测改成 45 秒。
2026-10-05 16:10:55 +08:00
ThearchyHelios 1491b5c7f3 双盲复算复核整改:指标自己的筛选范围不再写成有效记录条件,盲算不再看到原流程的列名,差异说明收窄到这一格
写数据目录(dual_check_facts.py)
- 有效记录条件只收「排除无效记录」的条件:被排除的取值是退款、作废、测试这类(或码值表里写着这类意思),
  标记列只认留下有效记录的一边(is_test = 0、is_valid = 1、deleted_at IS NULL);取值出现在指标名称、口径说明、
  所在分组、运行输入里的,用到分组列的,一律不写,原因写明「可能是指标自己的筛选范围」。以前「华东销售额」的
  region = '华东' 会被记成订单表人工确认过的有效记录条件,之后所有查订单表的工作流(含下一次盲算)都只看华东
- 目录里已有的有效记录条件被采纳的一边也按它筛了的,和新条件合起来写(已确认的不改,照实说);没按它筛的不动。
  以前整项替换,两边都用着的 is_test = 0 会被静默丢掉。推断的去重规则说法不同的也不替换
- 业务日期只在两边都按时间筛、按的列不同时才写;一边根本没按时间筛的不写
- 采纳了不去重的一边不写去重规则:那是指标自己的算法,写成「不去重」会把别的指标带偏
- 同一次裁决里几个指标对同一项给出矛盾的说法,整组都不写

节点(nodes/dual_check.py)
- 盲算任务里不再给原流程结果的列名和同一行的维度列名:分组的只给取值(日期说成「2026-09-02 这一天」),
  原 SQL 写死的文字列连取值也不给;只有几项输入都来自查询又没写口径说明、列名看不出取舍时才给列名。
  以前「order_date 为 2026-09-02」等于告诉盲算按下单日归属,下单日和支付日的陷阱正好漏掉
- 差异说明先把两条 SQL 收窄到这一格(只留算出这一列的那一项,分组的按这一行的分组值改成等值条件)再比,
  多列查询、分组取一行时不再混进「选出的列」「分组」这类噪声,只差一个方面的也写得进目录
- 产出状态:全部一致才是 agreed,一个都没核成是 unchecked,核成一部分是 partial;产出和台账加 counts;
  无法复算的指标发 dual_check_unchecked {metrics: [{id, name, reason}]}
- 共用一格的几个指标触顶时每个指标各发一条 dual_check_limit
- 查询次数刚好用完、交回的格子是真查出来的照常比对(时长、金额触顶仍按无法复算)
- 审核人没选或选了认不出的照实记「审核人没有对指标作出选择」,不再替人记成「两边都不对」
- 盲算先单独做第一个,其余最多 3 个同时进行,避免串行撞上运行单段的时长上限

配置
- 画布校验:双盲复算配了失败重试报错(重试会把盲算整轮重做、再问审核人一次);enabled 用 is 判断,0、1 不再放过
- 发起时的核查设置快照:子工作流(按运行时的取法读)里有双盲复算、反方审稿节点时也快照
- 助手节点说明补一句不要给双盲复算配 retries

测试:补上遮罩两道防线、规范化后一致但值不同、两个输入都对不上、只复算配置的指标、原值为 0、摘要最多 3 条、
驳回、多列查询、分组取一行、指标筛选范围不写目录等用例;每条核心改动都做过去掉实现、测试变红的验证
2026-10-05 15:54:03 +08:00
ThearchyHelios eea098e975 运行对比加固(第 3 轮):单格要是同一个区域的同一行;归因按哪组维度说就按哪组维度核对;快照里行和列对不上的说不清;补单格反方向、行段只差最后一行、精确性一侧等变红测试
- 单格(数组字段的一个元素)引用的那一行两次是同一个区域,还要是它同样的那一行:按查询结果的先后,排在这一行前面的、它的行数两边一样。区域 × 日期的结果东区少了一天,第 3 行从南区的第一天变成第二天,原来报成只数据变、南区少了 5(同一天的南区其实多了 10,正负号反了),同一处引用换成一行的行段却说不清;按金额排的第 2 行从东区第二高的那天变成最高的那天、取第二个元素的也一样。只比排在前面的行数,不比外面还有没有它的行:东区两天还是一天、第 1 行都是东区第一天的照常是数据变化
- 行段核对引用的是不是同一批维度值时,按两边整份结果认出的维度核对一遍,再按归因用的维度(引用的那段行上认出的)核对一遍,两组都切在同一处才算同一处。渠道在引用的行里两边都只有「线上」、归因按区域说:南区第二天走了线下、挤到第 5 行,原来只按(区域, 渠道)核对,报成只数据变、南区 -100%,和「南区真的只有一天」一字不差,反方向也是;区域 × 渠道取前 2 行、南区新开的线下排第 3,归因 300 → 280,快照里南区是 300 → 480。只按归因用的那组也不够(只引用一行的认不出维度;引用的行两边都只有东区、只有整份结果分得出区域的也要核对)。取行的规则抽成 _attributed_rows,归因和核对共用
- 快照里有的行和列对不上、认不出各行是哪个维度值:原来退回按行号认、报成只数据变,现在和快照读不出来一样说不清,说清是哪一边
- 新测试:单格同一区域换了一行(两个方向、按金额排、取第二个元素、排在前面 2 行对 1 行、只有一行的行段同样说不清、同一行的对照);按归因用的维度核对(渠道挤出、反方向、新开线下,各带对照,占比之和为 1;只引用一行时仍按整份结果的维度核对);行和列对不上(两个方向);单格反方向(这次取的区域在基准运行里,只是不在这一行);行段只差最后一行;精确性一侧(第 4 名每周不同、后面剩的行数不同,仍是数据变化);排在前面的行数 2 对 1、位置说明里的行数、两边都只引用一部分但行数不同、基准一行都没引用时说哪一边;引用的行两边都只有东区时按整份结果认区域;金额列存成文本时也不当维度;只在一次运行的图里有的下游节点;不归因时整体说明照样提遮罩
- test_an_element_only_needs_the_same_region_on_its_row 改名 test_an_element_does_not_mind_other_rows_of_its_region:单格不再只看区域,原名和说明都不对了
2026-10-05 15:52:51 +08:00
ThearchyHelios 4d364d385b 反方审稿复核修正(第 2 轮):照录的理由不露内部标记、裸数字注明无出处,只停用了部分数据源时理由和时间线写明没查哪几个,撞上每日金额上限的几句同一种说法
- 不采信时照录反方的理由改成纯文字:解析不了的引用(⟦?v:…⟧)换成「(引用无效)」,裸数字后面注明「(无出处)」。
  这种句子没有理由文档,界面按文字显示,以前内部的占位标记会原样露在审批卡上,裸数字也不标
- 一句该查的数据源只停用了一部分时照常反查,理由末尾由系统注明哪几个没查(写进理由文档:界面按文档画理由,
  写法和把说明写进原文再组装出来的一模一样),时间线另记一条 challenge_sources_lost。以前只写进给 agent 的任务,
  审稿记录和审批卡上的「经受住」看着就像每个源都查过了。没挂依据的句子按整份报告用到的源查,任务、理由里不再说
  「这句引用的」
- 当天的核查金额用完时,派出去就撞上上限、一次模型都没调用的那句,和后面不再派出的句子用同一种说法
  「已达上限(…),这句未反查」,不再一句「反查未完成:…没能交结果」、一句「这句未反查」
- 判失败的报错只推翻一句时说「请核对这句结论」

没有改的:被推翻的出具缺口读成「反方审稿「反方审稿」:反方审稿推翻了…」并归在「校验不完整」下,审批卡和节点产出
分不出根本没反查,challenge_unreviewed / challenge_sources_lost 不在期 3 约定的日志代码表里——这几处要改约定或
别的包的文件(io.py、decode.ts、NodeCard.tsx、EvidencePane.tsx、ChallengeSummary),留给编排者定
2026-10-05 15:50:20 +08:00
ThearchyHelios 4ceac6b74c 反方审稿复核修正:没有出处的结论不采信,推荐接法下审批卡有报告,重试和接着跑不重审、不拿旧审稿冒充
- 出处:一条查询都没执行成功的判什么都记为无法判断,判「被推翻」的理由里还得引用到自己的查询结果——零查询的推翻能让
  正式出具降档,零查询的「经受住」会给审批人假安心。系统写明原因,反方原本的结论和理由照录。这句引用的数据源都已
  停用时不派 agent;核查模型本身用不了时后面的句子照同一个原因记,不再逐句去建模型
- 时间线:有句子没能反查时另记一条 challenge_unreviewed(带第一条原因),challenge_done 的说法分开写查过的和没查成的
- 审批卡:「报告 → 反方审稿 → 人工审批」时审批说明不引用正文也认得出这份报告(reviewed_report_ids 穿过直接连着的
  反方审稿,reviewed_by_human 同步),审稿概况附得上
- 重试与接着跑:同一次执行里的重试沿用已经审完的结果,不再多花一份钱、也不会把「被推翻」重审掉;审稿结果带输入指纹
  (报告、文档、反方模型、上限、遮罩),接着跑时只改「结论被推翻时」沿用,改了审稿本身就重审
- 处置:产出和审批卡里的 on_refuted 写实际生效的 degrade / flag;「报错并停止运行」配「出错时:记录错误并继续」画布
  校验挡住,跳过校验的旧图执行时改按出具降档;反方审稿不在带出具契约的成果节点上游时画布给警告(降档不会发生)
- 摘录里指向原报告查询的写法(指标取自查询 Qn、查询 Qn 的 SQL、出现在查询 Qn)都标成「原报告 Qn」,和反查 agent
  自己的 Q1 分开
- 选项叫法和检查器下拉一致:出具降档 / 只标注 / 报错并停止运行,「启用」写跟随设置;助手手册同步
- 补测试:快照里的反方模型和每日上限、裁判判成不是结论的句子不审、审批卡只给被审的那份报告挂概况
2026-10-05 15:08:53 +08:00
ThearchyHelios 44e4597037 运行对比加固(第 2 轮):行段要把引用过的每个区域切在同一处——一边整个被引用、另一边只引用了一部分的,引用的行数没变少也说不清;只在一边有的区域那一边算整个被引用;引用的行不从第 1 行开始时还要是它同样的那几行;单格只看那一行是不是同一个区域
- 一个区域在一边整个被引用、另一边只引用了一部分(其余的行还在快照里):上一轮只在「有行在外面的那一边引用得少」时判说不清,南区这次多了一天、引用的行只盖住前两天(行数没变少,甚至更多)照样报成只数据变,归因和「真的只有这几天」的对照一字不差,反方向连正负号都反了;现在按切法比:两边都整个被引用,或者都只引用了它同样的那几行,才是同一组数
- 只在一边的快照里有的区域,没有它的那一边算整个被引用:北区这次新出现、三天只引用了两天,基准运行南区只引用了一天、这次南区没了,都说不清;新出现、没有了的区域整个被引用的照常是数据变化
- 引用的行不从第 1 行开始时,按「排在引用的行前面的行数、引用的行数」认引用的是它的哪几行:两边都只引用了东区一行,基准是它金额第二高的那天、这次是最高的那天,说不清,说明里写出两边排在前面的行数
- 单格(数组元素)只看引用的那一行两次是不是同一个区域,不套行段的切法:东区两天还是一天,第 1 行都是东区的第一天;第 1 行的东区真的没有了照常比
- 说明说引用的行外面有它的那一边,两边都有的说引用得少的那一边;几个区域切法都不同时说基准运行引用的行里先出现的那个
- 截断的快照仍按取回的行比(和引用整份结果一致,挤到截断点后面的行看不到,不做归因),写进文档
- 新测试:一边整个一边部分(这次多一天、这次引用得更多、反方向,各带真的只有这几天的对照,占比之和为 1)、切在同一处(两边都只引用南区第一天仍是数据变化)、行数不同两个方向的说明、几个区域都不同时说哪一个、排在前面的行数不同、只在一边有的区域(新出现、没有了)、两列维度一起认(含一边只有一个值的区域列)、单格只看那一行、挤出两行时的行号和行数(两个方向)、正好 3 行不写「等」、两个 Agent 都交得不同时点名画布上靠前的、单边遮罩时整体说明提到遮罩
2026-10-05 14:47:05 +08:00
ThearchyHelios e353706498 双盲复算写目录:人工驳回过的同一说法不再写回;节点说明补上盲算 agent 看得到什么
- 数据目录里这一项以前被人明确驳回、驳回的正是这次要写的说法:两次人工判断对不上,不替人拿主意,不写并照实说原因;驳回的是
  别的说法时照写。业务日期只按哪一列比,规则、时区是补充。
- 节点模块说明和盲算的系统提示补上:结果分了组的另给那一行的维度,统计期看本次运行的输入。
2026-10-05 14:31:15 +08:00
ThearchyHelios ce19b074cf 双盲复算:盲算 agent 的任务里带上本次运行的输入参数
原流程的 SQL 常常是拿运行输入拼出来的(统计期的起止、地区),盲算 agent 只看口径说明不知道算哪一期,就算不出同一个数。任务里
加一行本次运行输入里的标量参数(最多 12 项、每项截到 40 字),只给参数、不给 SQL。分组结果的测试顺带核对任务里有统计期参数、
没有原流程算出的数。
2026-10-05 14:27:19 +08:00
ThearchyHelios 77f3119cf9 双盲复算节点:按输入盲算、代回原式比对,不一致交人裁决,只差一个方面时写进数据目录
占位的直通桩换成真实现(engine/nodes/dual_check.py):
- 按输入复算:口径卡上输入来自查询快照的指标,每个这样的输入另起一个看不到原 SQL 的只读 agent(verify_agent),只给它指标名、
  口径说明、这个输入在计算式里是哪一项(「输入1」,不露取数节点和 cell() 的行列)、分组结果那一行的维度列、数据源的数据目录;
  它交一个格子引用,由代码从它自己的快照取值。盲值用原式代入盲算的输入算(engine/recompute.py,取整同口径卡),按相对误差比,
  原值为 0 时按绝对误差。两个指标用到同一格只盲算一次。合并查询、整段结果、遮罩列、文本输入不能单独复算,记「无法复算」、不花钱。
- 一致不挂起,产出 agreed;不一致时逐个对不上的输入用 explain_sql_diff 说清差在哪(规范化后一致却值不同的写「差在数据」),
  完整对照落成 card_artifact 工件(避开审批 payload 的截断),按约定的 payload 用 ask() 挂起。审核人选 a 无缺口,选 b、都不对
  写 issuance_gaps,on_reject 为 fail 时节点失败;明确驳回按人工驳回失败;没选的按「都不对」。触顶、核查模型不可用、交不出数的
  指标记「无法复算」并写缺口,核查模型本身用不了时后面的盲算不再一个个去试。
- 设置读发起时的快照,没快照的读一次记进 checkpoint;读设置、盲算连同比对、差异说明连同工件、写目录各在一个 @task 里,审批恢复
  重放时不再调用模型、不再计费、不再写,事件也只发一次。日志按约定的 code 和结构化字段发(dual_check_skipped / same_model /
  agreed / disagreed / resolved / catalog / limit)。盲算的查询只发 tool.end(role: verify),台账只记一条 kind: dual_check,
  报告的写作目录不含盲算的查询;用量计入节点。

选择写进数据目录(engine/dual_check_facts.py):只在审核人选 a 或 b、差异只有一条具体条目、又对得上目录事实时写——时间列不同记
业务日期,一边多一个只涉及一张表的筛选条件记有效记录条件,聚合只差去不去重记去重规则,都按被采纳那一边的写法。照对话提案的
规矩核对,记为人工填写、已确认,备注写明运行、节点、指标;已是同样的已确认说法不再写,确认过的不同说法不覆盖、照实交回原因。

测试:虚构零售库埋了退款行、下单日对支付日两个陷阱,盲算模型是 MockChatModel 剧本,覆盖一致、不一致挂起与 payload / 工件形状、
三种选择与 on_reject、写目录与幂等、不覆盖、全局关闭与节点覆盖、发起时快照、重放不再执行盲算、写作目录不含盲算查询、用量、
同模型提示、触顶、不能单独复算的输入。占位节点的旧测试只留反方审稿。
2026-10-05 14:23:43 +08:00
ThearchyHelios ff23c6a8ad 双盲复算的配置与发起时的设置快照:画布校验、核查设置快照进运行选项、助手节点说明
- 画布校验(engine/schema.py):「口径卡」必须是上游的口径卡,没写时上游只能有一张(dual_check_card,节点执行也用这一个认法);
  「复算的指标」「口径说明」的键必须是那张卡上的指标 ID(引用自别的工作流的口径卡画图时查不出,不查);开关、允许的相对误差、
  未采用原流程时的取值。报错写检查器上的中文叫法,叫法暂放在 DUAL_CHECK_FIELD_LABEL(labels.py 不归这个工作包)。
- 发起(engine/launch.py):图里有双盲复算或反方审稿节点时,把核查设置快照进 options["verify"]。这两种节点在运行中途读设置,
  审批停下期间改了设置,恢复后的那一段就会按另一套规则走;发起时定下来,重放和正式运行才可复现。没有这两种节点时 options
  原样不动,run.started 和以前逐字相同;调用方已经给了 options["verify"] 的照它的。
- 助手的节点说明(NODE_REFERENCE)加 dual_check 一条:要正式出具、口径卡上的指标来自查询时接在口径卡之后、报告之前,各配置项
  怎么写(口径说明按业务写、盲算模型默认不写)。
2026-10-05 14:23:00 +08:00
ThearchyHelios dbf7b994d7 反方审稿:校验找不到报告时给一句实在的话(不留空串),节点说明写清「启用」的取值规则 2026-10-05 14:03:37 +08:00
ThearchyHelios 69b40c0b43 反方审稿测试补两处:摘录按数据源当前的遮罩也要遮,撞上每日金额上限之后不再派 agent
去掉实现逐项验证时发现两处测不出来:遮罩只测了查询当时就设好的情况(快照里自带遮罩列,不传当前遮罩也遮得住),
改成同时测运行中途才给数据源设上遮罩;每日上限只看了结果(后面的句子反正也会被拒),补上「只派了一次」的断言。
2026-10-05 14:00:59 +08:00
ThearchyHelios fe2f81c1c0 助手的节点手册加反方审稿:多个来源汇总的分析报告、要正式出具的报告加这个节点,接在报告之后
写清什么时候加、接在哪里(report → challenge → human 或 output,审批节点的说明里引用报告正文才会附上审稿结果)、
各配置项(要审的报告、三个上限、反方模型、结论被推翻时怎么办、启用)和默认:上限一般不写、取设置里的默认,不替人
把核查的花费写死;反方模型建议和写作模型不同。测试拿画布校验认的配置项和取值来对手册
2026-10-05 13:57:49 +08:00
ThearchyHelios dc5a113e29 反方审稿节点(后端):报告写完后挑几句关键结论,各派一个只读 agent 站在反方写查询找反例,被推翻的让出具降档
结论句裁判只看写作者挂的证据,可证据本身就可能挑过(只查了含退款的销售额)。反方审稿换个方向,自己查库去证伪:

- 选句照裁判的预筛(judge.candidates):挂了依据的、带比较或因果的在前,取前「最多审几句」句,裁判判成不是结论的不审
- 每句一个核查 agent(verify_agent.run_verify_agent):工具只给这句引用的查询所在的数据源(指标按输入取自的查询、合并查询
  按它合并的源算;没挂依据的在整份报告引用的源里查);任务给这句结论和它挂的证据摘录(judge.prepare,遮罩照旧,编号改成
  「原报告 Qn」免得和 agent 自己的 Q1 混淆);理由里的数字只能引用自己的查询格子,裸数字被标出
- 审稿记录 challenge_artifact 按约定的形状随运行封存;台账只记 kind challenge,查询不进任何报告的写作目录
- on_refuted:degrade(默认)写 issuance_gaps 让出具降档,fail 判节点失败,flag 只记录;产出带实际的 on_refuted
- 上限:句数、每句查询数、每句时长、全局每日金额,都按发起时快照的核查设置(run.options["verify"],没有快照按设置当前值),
  节点上写了的盖过它;全局关闭、报告没有可审的结论句、报告文档读不到时直通并记 challenge_skipped
- 日志按约定的 code 带结构化字段:challenge_skipped / challenge_same_model / challenge_limit / challenge_done / challenge_refuted
- 整个反查(连同存记录、发日志)放在一个 @task 里:接着跑时不重复查库、不重复记账、时间线不重发
- 审批卡:human 的 _reviewed_reports 给被审的报告附上 challenge 概况(counts、最多 10 条 items、artifact、on_refuted),
  没审过就不加这个键
- 画布校验:要审的报告必须是上游的报告撰写节点(不写时取直接连着的那份或上游唯一的一份,多份要人选);各项配置写错报中文、
  执行时报同一句;反方模型和写作模型写成同一个给警告
- 占位期的直通测试只留双盲复算(反方审稿的直通行为改由 test_challenge_node.py 覆盖)
2026-10-05 13:57:38 +08:00
ThearchyHelios 36848f0a1e 运行对比加固(第 1 轮):一个维度值占几行时按维度值数两边各引用了几行,有一部分挤出引用的行也判说不清;单格引用的快照读不出来时和行段一样说不清;补循环回边、写入者不知道时的措辞、严格拆分的范围、单边遮罩四处测试
- 区域 × 日期这类结果(日期不当维度),东区多了一天把南区的第二天挤到引用的行外面、仍在快照里:原来只记每个维度键在引用的行里外各自第一次出现的位置,两边都引用过南区就查不出,报成只数据变、南区少了一半;现在某个键在一边引用的行更少、那一边快照里引用的行外面还有它的行,判说不清,说明写「南区这次有 1 行在查询结果第 5 行,不在引用的行里」,两个方向都查;真的少了一天的照常是数据变化、占比之和为 1
- 遮罩列仍不当维度:只用没遮的列认,说明里只写没遮的列的值和行号;整个挪出去的维度值列出全部行号(最多 3 个,多的写「等」)
- 单格(数组元素)引用、一边快照读不出来时原来退回按行号认、报成只数据变,现在和行段一样说不清,说清是哪一边
- 新测试:循环回边上的节点算口径卡之前、写入者不知道时并行支路只说「节点」且不说「它的上游节点」、严格拆分不被口径卡自己和下游挡住、只有一边遮了区域列(两个方向)时说明、归因、整体说明里都不出现区域名
2026-10-05 13:14:41 +08:00
ThearchyHelios e626b205b6 合入 SQL 差异说明重做(阶段性):先判等价再说差在哪——规范形式一字不差才算等价,具体说法只说有把握的,其余给兜底并附规范化后不同的行;等价结论用执行比对守住 2026-10-05 12:58:42 +08:00
ThearchyHelios 77b5509cf1 合入期 2 变化三分法(后端):同一工作流两次运行的指标变化拆成数据、口径、模板与 SQL,口径与数据都变时能严格拆分两种效应,数据变化按维度归因,每项追到两边的查询快照;基准优先取同一定时任务的上一次 2026-10-05 12:49:30 +08:00
ThearchyHelios 36569e1fa4 运行对比复核修正(第 3 轮):行号相同的引用还要引用同一批维度值,记不出写入者时只看口径卡之前的节点,默认设置的登记按节点类型逐类核对
- Agent 两次引用的行号相同、都不是整份结果时,一边引用了的维度值(区域……)在另一边的快照里却没被引用(按区域排序多了北区、按金额取前 3 名名次变了),判为说不清,说明写出是哪个维度值、在第几行;不再把还在快照里的区域算成「数据没了」、给出远超 100% 的占比。另一边快照里真的没有了的照常是数据变化。数组字段里的单个元素同样核对。维度按两边整份结果认,遮罩列不拿来认,分不出维度的仍按行号认
- 两边都记不出写入者时,范围从整张图改为口径卡之前可能执行过的节点(它的上游加并行支路):口径卡自己、只在它下游的写周报写不了它读到的值,不再被点名成来源;并行支路上的节点不说成「上游」
- 「整份结果」统一按引用了哪几行判断,容器一支不再单列
- 默认设置登记表的核对测试改为按节点类型:从各执行器出发顺着用到的函数(含就地 import、回调、类)找 cfg 读法,在字面量元组上循环读的键名认得出,认不出的变量键名直接失败;核对节点模块里每个读配置的函数都走到了,登记了不读的也算错。据此补登记成果节点出具契约读的报告节点三项(evidence_from、metrics_from、entities)
- 补测试守住上一轮的几条规则:Agent 交的不同而代码节点的数没变是没变,只有这次运行的快照读不出来时说这次,默认设置同时改了结论句裁判时 Agent 只写模型,调不调模型由默认设置决定,报告节点在代码节点上游,改了几个节点时点名画布上靠前的
2026-10-05 12:28:38 +08:00
ThearchyHelios 02b08f9ad4 SQL 差异说明重做(第 2 轮补充):在真实的 PostgreSQL / MySQL 上复查,浮点数求和的一段不调换先后、外层条件只把聚合结果和「相等就是一样」的分组键挪进 HAVING;排列数先算再展开
为什么:第 2 轮的主体提交只在 SQLite 上执行比对。这次私起了 PostgreSQL 18 和 MySQL 8.0(复验者语料的那份数据),
拿复验者的全部语料和自己针对新改写造的 22 对重跑,抓到两处在真实的库上结果不同、却判了等价的:
- 套在子查询里的 UNION ALL 调换各支,外面 SUM:浮点数加的先后变了,1902.2 对 1902.2000000000003(PG、MySQL)。
- 外层 WHERE BINARY t.city = 'SH' 挪进分组子查询的 HAVING:MySQL 自己会把外层只涉及分组键的条件推进子查询的 WHERE
  先筛行,不分大小写时和 HAVING 结果不同。

做了什么(backend/app/data/sql_semantics.py):
- _Sensitivity:PostgreSQL、MySQL 里对不知道是不是精确数(整数、定点小数)的值 SUM / AVG / STDDEV / VARIANCE,这一段
  算「结果看行出来的先后」,连同给它供行的子查询、CTE 不调换来源、不排分组键、不并子查询。SQLite 不算(它用
  Kahan-Babuska-Neumaier 求和,关联先后也不跟着 FROM 变;语料 120 对在 7 组数据上按精确值比没有差别)。
- 外连接按 WHERE 变成内连接、内连接的条件挪进 WHERE:三种库的优化器自己也做,执行先后不变,看先后的一段也照做;
  只有 SQLite 写了 CROSS JOIN 的不挪。
- 外层 WHERE 挪进分组子查询的 HAVING 只挪聚合结果和「相等就是一样」的分组键上的条件;引用了好几次的简单 CTE 有
  一处在看先后的一段里就不拆开(PostgreSQL 会先把它算出来存着)。只筛行选列的子查询照样并进求浮点数和的外层:三种库
  自己也会并(PostgreSQL 的 subquery pull-up、MySQL 的 derived_merge、SQLite 的 flattening),执行先后一样(在两个
  库上核对过结果一字不差)。
- 同名来源的排列先算个数再展开:十几个同名表时以前先把十几的阶乘个排列都列出来(一条 12 张表自连接的语句跑了两分钟
  以上),现在超了上限直接按排好的先后。
- 模块说明照实写清上面几条和 SQLite 求和的假设。

测试:REGRESSIONS_R2 补这两处(PG、MySQL 的 UNION ALL 求和、MySQL 的分组键条件);DIALECT_SAME 里两对需要在求和的
一段调换关联先后的 PG / MySQL 改写移到 KNOWN_UNRECOGNIZED(只给兜底说明)。

验证(最终代码):tests/test_sql_semantics.py + tests/test_verify_agent.py 601 passed,tests/test_sql_equivalence.py
375 passed,全量 -n 8:7856 passed、11 skipped。等价改写 124 对认出 120 对(97%),不给表结构 117 对(94%)。复验者
vsadv 全部语料(928 次判定,SQLite / PG / MySQL 执行)无 blocker、判等价的没有一对执行结果不同,自己针对新改写的 32 对
同样;advfp 180 对等价对上具体说法 0 条;vsprobe 变异压测(种子 9、10)约 1.6 万对无误判等价、无异常;自己的压测
约 1.65 万条可执行,规范形式相同的组执行结果全一致。
2026-10-05 12:16:20 +08:00
ThearchyHelios f16860f704 SQL 差异说明重做(第 2 轮):规范化只留能讲清为什么保持语义的几步,结果看行出来先后的一段不动来源;具体说法先确认两边真的不同、另一边真的没有,其余给兜底
为什么:第 2 轮复验抓到第 1 版规范化里一批不保持语义的改写(15 个 blocker),说明里一批假话、矛盾话(10 多个
major)。根子是借用了 sqlglot simplify 等现成改写、又把「相等」当成「一样」、没管结果看行出来先后的查询;说明这边
是先改名再比、只看这一段有没有那个条件。这一轮把改写收窄到每一步都讲得清为什么保持语义,说明这边每句话都先验证。

规范化(backend/app/data/sql_semantics.py):
- 去掉 sqlglot 的 simplify(Decimal 折叠 0.1 + 0.2、-(-x) 化成 x、三项加乘拍平合常量、BETWEEN SYMMETRIC 丢了
  SYMMETRIC)、pushdown_projections、HAVING 挪进 WHERE(MySQL 不分大小写时同组的值各不一样)、只有一列的 IN 列表
  写成 =(SQLite 里 a IN (b) 是 a = +b)。只留自己写的几步:AND / OR、比较两边、一个节点的加乘两边排序,NOT 套
  比较翻过来,BETWEEN 拆开,IN 常量列表去重排序,COUNT(1)、MIN/MAX(DISTINCT)、IIF / IF 写成 CASE、CASE ELSE NULL、
  恒真的 1 = 1 去掉,IS NOT NULL / NOT LIKE 各方言统一成 NOT (…)。
- 新增 _Sensitivity:取行(含有 ORDER BY 并列)、DISTINCT ON、窗口函数、GROUP_CONCAT 这类聚合、聚合里的散列、当成标量
  的子查询,以及去重、分组、MIN/MAX 留下的值「相等却不一样」(MySQL 字符串、PG numeric、算出来的表达式)的一段,
  连同给它供行的子查询、CTE,都不调换来源、不排 GROUP BY 键、不改 DISTINCT、不并子查询。SQLite 的逗号连接和 CROSS
  JOIN 按原文分开(SQLite 不调换 CROSS JOIN 两边)。
- 列名表(CTE、子查询)写进里面那段的别名,实表别名带列名、递归 CTE(含 SQLite 不写 RECURSIVE、CTE 引用自己或后面的)、
  BETWEEN SYMMETRIC、FETCH WITH TIES 不走规范化;FETCH FIRST 写成 LIMIT;USING 只在简单写法下写成 ON。
- EXISTS 改写前先查聚合(含 ORDER BY 里的)和 OFFSET;合并只调换最外层不取行的 UNION ALL(PG/MySQL 另加按位置排序、
  各支列名一样的嵌套);后面有 RIGHT / FULL JOIN 时内连接不挪进 WHERE、开头的来源不调换。
- 子查询并进外层自己写(_merge_derived:只并内连接一侧、只筛行选列的),引用多次的简单 CTE 每处写回一份后整条语句
  的别名重新改成互不相同的(压测撞到的:同一个 CTE 并进外层和相关子查询两处,里层别名遮住外层);外层 WHERE 只挪进
  分组子查询的 HAVING(同一个值)。几个来源的 SELECT * 先写成各来源的 t.*(sqlglot 展开 * 时把实表排在子查询前面)。
- 解析丢了写法的不在语法树上判等价:0x10 和 x'10' 这类字面量、换了名字的类型名(SQLite 的 STRING 是数值亲和);
  SQLite 不带表名、带双引号、表结构里没有的列名保留大小写和引号(找不到这一列时被当成字符串)。

差异说明:
- 换了表的一边照它自己的表说(以前把 B 改名成 A 的表,中文名、detail 归错表)。
- 新增 _Range:同一个对象只和常量比的条件按取值逐段算,留下的行一样的不说(IN 和 OR、冗余 IS NOT NULL、拆成两段的
  时间、COUNT(*) > 1 和 >= 2);关联条件比相等关系的分组;聚合只在参数是列时说。
- 新增 _Usage:说「另一边没有这个条件 / 没有按时间筛选」之前,看另一边整条语句(含相关子查询、CTE、CASE、判空时连
  聚合参数)是否用这一列筛选;条件子查询里不带表名的列没给表结构时不拿来说具体的。
- 子查询要外面写法一模一样(含 NOT、IN 左边的列)才配对,位置照实说「id NOT IN 的子查询」;EXCEPT 按先后逐支比;
  order 只在最外层、没取行、没 DISTINCT ON / 窗口函数时说;LEFT 对 INNER 只在最外层、可空一侧没进 WHERE、结果不只是
  可空一侧的聚合时说;DISTINCT 对同列 GROUP BY、MySQL 表名只差大小写不说成不同;互相矛盾的一对不说。

测试:tests/fixtures/sql_pairs.py 加 nums 表和一组专挑写法差异的数据(0.1 + 0.2、像数字的文本、'SH' / 'sh'),
第 2 轮反例收进语料(_SAME_HARD 20 对、_SAME_R2 11 对、_DIFF_R2 36 对进执行比对;REGRESSIONS_R2 21 对、
DIALECT_SAME_R2 6 对、KNOWN_UNRECOGNIZED 4 对各方言只查结论);执行比对共 7 组数据、372 对,标 same 而没认出的
不许有具体说法;test_sql_semantics.py 补第 2 轮的说法用例和 normalize_sql「只改参数」的用例。这些新用例在上一版
上有 109 处失败。

验证:test_sql_semantics + test_verify_agent + test_sql_equivalence 973 passed;全量 -n 8:7853 passed、11 skipped。
等价改写 124 对认出 120 对(97%),不给表结构 117 对(94%)。复验者的脚本:vsprobe 变异压测 6 个种子约 4.3 万对无
误判等价;在私起的 PostgreSQL 18、MySQL 8.0 上重跑 vsadv 全部语料无 blocker;advfp 的 180 对等价对上具体说法 0 条;
自己的压测 3 个种子各约 1.5 万条可执行,规范形式相同的组执行结果全一致。
2026-10-05 12:01:08 +08:00
ThearchyHelios 3254744f6d 运行对比复核修正(第 2 轮):行段相同、一边是整份结果的引用不再算同一处,默认设置的变化只落到会读它的节点上,上游 Agent 交出的结果不同时不再说变化来自上游数据
- Agent 两次引用同一段行,一边是整份结果、一边不是(这次多了北区,没引用的西区还在快照里):判为无法比较,写明哪边引用了整份结果、另一边引用了哪几行、结果共几行;快照读不出来、核对不了是不是整份结果的同样不算同一处。以前按同一处算,西区被算成「数据没了」,占比超过 100%
- 图级默认设置的变化按「哪类节点运行时会读这一项」落到节点上(_CFG_KEYS,照各执行器经 NodeContext.cfg 读的配置项登记):结论句裁判、结论句的处理只有报告节点读,不再把 Agent 记成配置变了;条件分支让模型分类、结构校验让模型修复、知识检索用模型重排时也读默认模型,同样落到它们身上。新测试按执行器源码核对这张表,执行器新读了一项没登记就失败
- 代码、数据整形这类节点的上游有 Agent、模型调用、人工审批等节点,两次交出的产出不同(工件按内容寻址,按工件 id 比;产出没落成工件的核对不了):它交的数变了不再说变化来自上游数据,判为无法比较并点名是哪个节点
- 上游节点改了配置时说明里点名是哪个节点;默认设置那句拆短,写出跟着变的是哪几项(模型接入不写 provider)
- 补测试守住复核列出的未覆盖规则:容器运行输入还原不了、合并查询的输入追不下去、上游只有运行输入变了、审批默认设置、图里 SQL 只改格式(代码节点在下游)、跨节点先配字面量相同的、少了一次查询、只有一边落在快照上、查过库的写入节点改了配置、代码节点上游有节点改了配置、旧口径叶子的出处说不清时不拆、单格引用换了行;变异 99 处全部打红
2026-10-05 11:28:41 +08:00
ThearchyHelios d176ff77ff 运行对比复核修正(第 1 轮):输入的出处追到合并查询的上游、Agent 引用的列和行段,没出处的数、数据没变数却变了的不再报成数据变化
出处判定(每个指标的分类靠它):
- 合并查询:读合并结果的指标按别名一路追到喂它的每一份上游快照(嵌套的合并也追),上游 SQL 结构变了是 sql_changed,
  不拆、不归因;合并节点多接、少接了输入也算结构变化
- 图里写死的字面量改了('paid' 改成 'refunded'、系数、LIMIT):执行的 SQL 只差字面量,但图里的 SQL 改过,算模板变化;
  图里一字未改(定时运行代入的日期)才是参数。图里的 SQL 只改了空白、大小写的不算改过
- Agent 按出处交来的字段:还要引用同一列、同一行(段),两边都引用整份结果的不算换了;换了列、换了行段的说不清
  (incomparable),不再把没引用的区域算成「数据没了」去归因
- 模型、人直接交出的值(Agent 没按出处交、模型调用、人工审批……):值变了说不清是数据还是模型本身,判 incomparable;
  它自己查的库结构变了、配置变了照常标出
- 代码等确定性节点:上游的查询结果两次一模一样、交出的值却变了的,判 incomparable,不再说「变化来自上游数据」;
  上游只有运行输入变了的算参数
- 图级变化落到节点上:连线有增删的终点节点、默认设置改了且没有自行设置这几项的模型类节点,都算改了配置;
  「模板和取数的 SQL 都没有变化」只在整张图都没变时说
- 运行输入是列表、对象的,按还原出来的完整值比,长度不变、内容变了的是参数变化
- 同一节点按 SQL 配对时先配字面量也相同的:Agent 换了先后查东区、南区,两条各自配上自己,不再配成两条「只有参数不同」
- 一边记不出输入是哪个节点写的,说清是哪一边没记录(不再写出「None」)

口径与数值:
- 口径变了的指标,喂它的取数也变了、说不清的照样写进说明(只在一边才有的输入是改表达式带来的,不重复说);
  分类次序写进 compare() 的说明
- 千分位和不分组互换只影响显示,写进说明、不算口径变化;比率与否不同才算
- 升级前的口径卡没记小数位、显示格式:按那次运行图里的指标定义核对,定义也没有就不比,不再误报「小数位由 未设置 改为 1」
- 百分比:基准极小、这次很大时十进制量化放不下,退回浮点算,不再整个对比报 500
- 没设小数位的指标拆分:三个数按同一精度量化(最多 15 位有效数字),转成浮点交出去后两效应之和仍逐位等于总变化
- 维度归因标出用的是哪个输入、两边各自哪份快照(path、base_artifact、target_artifact)

接口:已经结束、只是没有封存清单的运行,409 说「没有封存清单,不能用于对比」,不再说「运行结束后才能对比」。

测试:补上复核指出的必测项(只一边对得上不给占比、只截断基准一侧、换了数据源、按旧口径小数位取整、写它的节点多查一次、
口径和 SQL 一起变不归因、小数位和比率变化、日期类型和常量列不当维度、合计行浮点误差、只改名称、结束过又接着运行的不当
基准),以及上面每一处修正的用例。32 处变异逐条打红验证,全部被测试抓到。
2026-10-05 10:20:29 +08:00
ThearchyHelios a9d7cba884 定时任务复核修正(第 1 轮):重启挂起的运行不再让任务静默停摆,格式选错、地址发不出去时照实报,整份回写不再丢掉到点的触发
- 挂起:上一次运行在服务重启时被挂起(interrupted、没有待审批)不会自己接着执行,之后每次触发都按重叠跳过,原因写
  「还没有结束」,一条通知也不推,任务就此静默停摆。interrupted 照规格仍算没结束,但跳过原因按挡路的运行分三种写
  (仍在进行中、正在等待审批、已挂起并请在「记录」中继续运行或放弃),运行编号写成界面上的 #前 6 位,去掉「按设置」;
  挂起的那次按 failed 事件推一次,写明原因和处理办法,不是每次触发都推。停在审批时每停一个审批点推一次(已处理状态
  带上第几段执行,以前第二个审批点不再推);审批刚批完、还没接着执行(answered)不当挂起
- 没有结束时间的失败运行(服务重启时还在排队、被改成 failed)永远不会封存:等封存改按最后更新时间算,以前按「现在」
  等,每一拍都等、永远不推
- 平台错误码按地址的主机认,不看所选格式:格式和地址对不上时,测试显示「已送达」、正式推送记成功,群里却收不到;
  现在记失败并带上平台的错误码。群机器人的地址(飞书、企业微信、钉钉)保存时要配它平台的格式,否则 422;格式省略时
  按地址认。只在地址或格式真改了时查,库里已有的旧设置不挡停用、改名
- 地址校验:端口不是 1 到 65535 的数字、夹着空格或不可见字符(零宽空格、控制字符)、httpx 建不了请求的(主机名不合
  IDNA 规则)保存时就回 422 和中文说明;推送时另兜住 InvalidURL 和其他异常,测试接口不再回 500 或英文报错;端口写错
  的地址也照样取出主机名
- PATCH 看值变没变,不看键在不在:整份表单回写不再重新校验没改的工作流(工作流删了也能停用),cron、时区、启用状态
  真改了才重算下一次触发。启用着改 cron、时区时,从原来那个到点的时刻接着算,新表达式也覆盖那一刻就还发起这一次。
  补两人同时保存的测试,证明条件 UPDATE 守住了乐观锁
- 发起中的占位:接口读到正在发起的触发按「已发起」给(run_id 为空),不再闪一次「未能发起:服务在发起运行前停止」;
  库里仍先写占位,进程被强杀时照实说没有发起
- stop:先置停止标志再取任务,这一拍收尾不再新起通知扫描(以前会落下一个不在等待名单里的通知任务);循环和通知两件
  事并行等,宽限缩到 3 秒和 1.5 秒,给关停时的 run_manager.shutdown 留出 docker stop 的大半时间
- 通知正文:立即运行写「手动触发」,补发标明「错过后补发」;失败不再写「原因:运行失败,详情请查看运行记录」,改写
  失败节点的名字和处理办法;状态叫法和界面一致(等待审批,已挂起,可继续运行);「封存哈希」改叫「清单哈希」,运行
  编号写 #前 6 位;测试消息只说收到它证明得了的事(企业微信、没配密钥时不提签名)。测试接口的 message 改成 ok、
  http_status 之外的补充说明(送达时提醒去接收端确认,失败时是原因),前端接在「测试消息已送达(HTTP 200)」后面不再重复
- 422 文案:不回显 Python 的值(None 写「未填写」,列表不原样回显、点出不认识的那一项),格式、事件写中文名;
  copy-allow.json 为平台名「钉钉」加一条(和前端包为 terms.ts 加的那条并存,试合并不冲突)
- test_loop_ticks_until_stopped:下一次放在整分上、只认发起的那一条,去掉约 1/60 的偶发失败
- 逐个做过变异,改回后对应测试都变红:挂起不推、跳过原因退回旧写法、没结束时间按现在等、错误码只看格式、去掉版本
  条件、到点保留和值比较一起去掉、不屏蔽发起中的触发、stop 既不查停止标志也不兜底、answered 当挂起、审批状态不带段号、
  不建请求校验地址、保存时不查格式
2026-10-05 10:00:25 +08:00
ThearchyHelios c55b1983f6 SQL 差异说明重做(第 1 轮):先判等价、再说差在哪——规范形式一字不差才算等价,具体说法只说有把握的,其余给兜底并附规范化后不同的行;等价结论拿 SQLite 执行比对守门
为什么重做:过去五轮按方面归纳任意 SQL,每轮修好一批又冒出一批(第 5 轮还回退过「只在 IN / EXISTS /
标量子查询里用到的 CTE 里的不同」),收不拢。改成先判两条 SQL 是不是一回事,不是的话只说有把握的不同。

做了什么(backend/app/data/sql_semantics.py):
- 新增 explain_sql_diff(a, b, dialect=None, *, schema=None, names=None, labels=("A", "B")) -> SqlDiff
  (equivalent、items、fallback、text_diff、level)。两边各自规范化,每一步只做保持语义的变换;同一级的两个
  规范形式语法树逐节点一样才算等价(不比生成的 SQL:sqlglot 生成 SQLite 时把 CAST(x AS DATE) 写成 DATE(x)),
  否则一律不等价、条目一定不为空。三级:optimizer(sqlglot 的 qualify、eliminate_subqueries、merge_subqueries、
  eliminate_ctes、pushdown_projections、simplify,给了数据源的 schema_cache 就按它认列、展开 SELECT *)、
  light(不认列)、parsed(只统一大小写、空白、引号)。
- 不用的 sqlglot 规则:unnest_subqueries(OR 里的标量子查询改成 CROSS JOIN,子查询没有行时整条都没了)、
  canonicalize(日期比较加 CAST,SQLite 里 CAST(… AS DATE) 取的是数字)、normalize(合取范式人读不懂);
  simplify 去掉同一列上比较的合并、字符串常量比较、等式移项、DATE_TRUNC 改区间(取决于列类型、排序规则、方言);
  merge_subqueries 不并外连接可空一侧算出来的列。
- 本模块自己的几步:去括号、ASC、OFFSET 0,COUNT(1)→COUNT(*),IN 列表去重排序,EXISTS 写成 SELECT 1,
  DISTINCT↔GROUP BY,原样透列的子查询换掉,RIGHT JOIN→LEFT JOIN,WHERE 筛掉了没关联上的行的外连接写成内连接,
  内连接写成逗号连接,起规范的名字(CTE 叫 cte1…、CTE 的列叫 c1…、别名按表名和层数,同名来源排列着试),
  交换律成立的地方排序。
- 具体说法的门槛:两段读的来源对得上(一样,或只差一处说得清的)才逐项比时间列、筛选条件、聚合与去重计数、
  选出来的列、分组键、HAVING、取行和排序、UNION 还是 UNION ALL;条件里的子查询两边各一个、外面一样的往里比
  (第 5 轮回退的 IN-CTE 反例在这里说出来);原文是 LEFT / RIGHT JOIN、规范化成内连接或调了方向的照原文说;
  牵涉 CTE 算出来的列、自己关联自己、说不准的子查询的不说具体的;两边给人看一样的不说。
- 兜底:一条具体说法都没有时给 aspect="other"「两条 SQL 的写法不同,未能归纳成具体差异,请对照原文」,只覆盖
  一部分时追加「另有未能归纳的差异,请对照原文」,fallback 为真;text_diff 是 difflib 比两边 sqlglot pretty
  的输出,最多 40 行。
- diff_facets 签名不变,交给 explain_sql_diff 比两份 facets 记下的原文,返回它的 items。normalize_sql 判
  「只有参数不同」不变,补了测试。

守门(tests/test_sql_equivalence.py、tests/fixtures/sql_pairs.py):虚构库(users、stores、orders、
orders_archive、order_items、refunds、payments、shipments)上 5 组固定种子的数据加 1 组极端数据(空值、重复、
边界值、悬空外键、空表)。305 对 SQL(113 对已知等价、192 对不许判等价,含复验者以前造的、在这几组数据上结果
不同的 91 对)在不给和给表结构两种情况下各判一次:结果不同的绝不判等价,标 diff 的不判等价,换个先后问结论
一样。旧测试五轮复核里报过不同的 234 对、意思一样的 56 对(SQLite / PostgreSQL / MySQL)原样收进来只查结论。

压测(变异 + 等价改写,每个种子约 1.87 万条)时找到、已经堵上的不保持语义的改写:
- merge_subqueries 把外连接可空一侧的常量列并进外层;merge 时同一个 CTE 并进外层和相关子查询两处互相遮住;
- 没给表结构时 qualify 把 WHERE / GROUP BY / HAVING 里和列别名同名的名字当成别名(SQLite 先认列),把 SELECT
  列表里和前面别名同名的名字展开(库里不认 SELECT 列表里的别名):这时不出 optimizer 这一级;
- UNION(去重)、INTERSECT 调换各支:去重留先出现的值(SQLite 里 2 和 2.0、MySQL 不分大小写时 'SH' 和 'sh');
  UNION ALL 带 LIMIT、按名字 ORDER BY 的也要看哪支在前,SQLite 合并结果的排序规则取最左一支:现在只在调换
  不改变结果时排;SQLite 从子查询读 UNION 的类型亲和;
- 一元加号在 SQLite 里去掉类型亲和、解析时却丢了;ORDER BY 别名换成表达式后撞上另一个别名;
- 没有 ORDER BY 的 LIMIT、聚合里既不分组也不聚合的列:调换来源、DISTINCT 写成 GROUP BY、RIGHT 写成 LEFT
  都会改变实际取到的行,这样的一段不做;两边都写了 COLLATE 的比较不对调。

验证:tests/test_sql_semantics.py + tests/test_verify_agent.py 519 passed,tests/test_sql_equivalence.py
307 passed,全量 -n 8:7706 passed、11 skipped。已知等价的 113 对认出 111 对(98%),不给表结构时 109 对
(96%)。压测 4 个种子各约 1.5 万条可执行:规范形式相同的组执行结果全一致,能执行的规范形式结果和原文一致
(带 CAST 的不比:生成 SQLite 时写法变了),生成的等价改写认出 2113/2115;复验者留下的 1201 对里可执行的 440 对没有误判等价,结果相同的 302 对认出 276 对。

仍做不到的(只给兜底或认不出等价)见模块说明:IN 子查询和 EXISTS 互换、窗口函数前后按分区键筛、三项以上的
加乘换结合、和方言有关的同义写法、SQLite 里套一层子查询的 UNION、去重 UNION 调换各支、LATERAL、递归 CTE、
没给表结构时别名和列同名的。假设列没有声明非默认的排序规则(SQLite 比较两列取左边那列的,表结构里看不到)。
2026-10-05 09:28:08 +08:00
ThearchyHelios 63c54e1c20 运行对比:compare() 的说明里写全返回的形状,接口说明指过来时前端照着就能读
除接口约定的字段外,几处补充也写明:模板节点的 labels(配置项的中文叫法)、输入的 masked 和两边的快照 id、查询的
两边快照 id、differences 和 problem;归不了因时 rows 为空、total_delta 为空、原因在 note。
2026-10-05 09:00:09 +08:00
ThearchyHelios 1e90a5a502 运行对比:结果里不出现非数和无穷大,维度列的非数当文字配对
接口按 allow_nan=False 写 JSON,结果里混进一个 NaN 或 Infinity 就是 500。指标值、输入值、十进制相减原本就只出
有限的数;剩下两处补上:维度列里的非数(float NaN)写成文字再当键,度量加起来、占比算出来超出浮点范围的给空。
2026-10-05 08:58:18 +08:00
ThearchyHelios 467f9599fd 运行对比:补合并查询、循环里多次执行的配对测试,口径卡读不出来时的说法补上「的」
合并查询只记在节点台账里、SQL 另在 merge.end 里,快照文件不在了照样比得了 SQL;循环里同一个节点执行几次的按先后
一一配上,多出来的一次标「删除」。口径卡指标集读不出来、被改过时的说明改成「口径卡「X」的指标集与哈希不一致」。
2026-10-05 08:55:57 +08:00
ThearchyHelios c89aaba10c 运行对比:Agent 换了查询先后时按 SQL 配对,Agent 改了配置时按出处交来的字段不再报成数据变化
查询配对改成一轮比一轮宽:同一节点、同一工具里规范化 SQL 相同的先配上,剩下的再按「第几次」配,最后跨节点按
SQL 配。以前严格按第几次配,Agent 这次换了查询的先后,两条都被配成「结构变了」,没按出处交的字段跟着被判成
SQL 变了。

Agent 按出处交来的字段:SQL 没变而 Agent 自己的配置(提示词、工具)变了时,它可能换了取哪一格,归为模板变化
(sql_changed)并写明是哪个 Agent;调用工具节点的格子由口径卡的 cell() 定,不受影响。
2026-10-05 08:52:04 +08:00
ThearchyHelios b66eb955b5 定时任务小修:数字按 isdecimal 认,输入生成失败的原因不再说成模板写错
- 「²」这类字符 isdigit 为真、int() 却认不得:写进 cron 或每页条数时会冒出英文的 ValueError(被全局处理器
  回成 400)。改按 isdecimal 认,照常回中文的 422
- 库里的 cron 或时区后来坏了时,立即运行记下的原因以前写「输入模板无法渲染」,其实模板没错:改成照实说
  「无法生成这次运行的输入」,后面跟是哪一项
2026-10-05 08:50:57 +08:00
ThearchyHelios 86e52192a9 运行对比后端(变化三分法):两次运行按指标拆清是数据变了、口径变了还是模板 / SQL 变了,数据变化按维度归因
GET /api/runs/{id}/compare/base 找默认基准:定时任务发起的先找同一定时任务更早结束的一次,否则同一工作流、
同一运行类别;只挑封存过的终态运行,找不到 404。GET /api/runs/{id}/compare?base= 对比两次运行:两次都要
已结束、封存且核对一致(照搬证据接口的封存判定),否则 409;不是同一个工作流、同一次运行 422。只读,不追加
事件、不落工件。

engine/compare.py 是纯函数:
- 查询按「节点 + 第几次 + 工具」配对,配不上再按规范化 SQL 配;normalize_sql 判 same / params_only /
  changed,结构变了的用 diff_facets 写出差在哪(时间范围排在最后);核查类查询不参与
- 口径卡同一节点按指标 id 配对,表达式、口径版本、小数位、单位、显示格式不同算口径变了
- 每个指标按 added/removed → 口径 → SQL 或上游配置 → 说不清 → 只参数 → 数据 的次序分类;输入落在快照上
  的直接比两边快照的 SQL,不落在快照上的看写它的节点和画布上游
- 口径变了时严格拆分:旧表达式配这次的输入用推演重算的还原和求值算 f_old(x_new),十进制精确相减,两效应之和
  逐位等于总变化;SQL 也变了、用到遮罩列、单位不同、旧叶子没记录的不拆并说明
- 维度归因按维度键配对两边的行、取变化最大的 10 个;度量之和等于输入的值(含合计行那一格)才给占比;截断、
  度量列遮罩照实写;遮罩列、日期列、每期都不同的列不当维度
2026-10-05 08:49:52 +08:00
ThearchyHelios bb176f6f9d 定时任务小修:输入模板的字段名去掉首尾空白再存,后台通知任务的异常记日志
- 「 stat_date」原样交给运行,入口节点认不出它、必填项照样报缺:保存时去掉首尾空白,去掉后重名的拦下
- 通知扫描是另起的后台任务、没人 await:出错时异常留在任务里,只会在回收时冒出一句「从未取回的异常」。
  包一层,出错记日志,下一拍照常
2026-10-05 08:47:18 +08:00
ThearchyHelios 6d11145a39 定时任务接口:增删改查、预览、变量清单、立即运行、触发历史和测试 Webhook
- 按前后端约定的 Schedule / Fire 形状返回;Webhook 地址和签名密钥加密存放,接口只给主机和末 4 位、有没有密钥
- 创建、修改时校验名称、cron、时区、工作流存在、正式运行要有发布版本(没有就说明原因、建议改探索运行)、输入
  模板和入口字段;不对回 422 {detail(中文), field},请求体自己解析,连 JSON 写坏了也回中文
- PATCH 带 version,条件 UPDATE 做乐观锁,对不上回 409;webhook.url 省略表示不改、空串或 null 表示删除,secret
  省略不改、空串去掉;只停用、改名不重新校验工作流——工作流撤了发布也要能先把任务停下来
- 预览给下几次触发(UTC)和按下一次触发渲染的输入,带工作流时只查字段名、不查缺的必填项(用户还在填)
- 立即运行和到点触发同一套规则(重叠跳过、正式运行只从当前发布版本发起),运行记在点按钮的人名下;触发时间
  取到秒,同一秒连点交回同一条,不会发起两次
- 触发历史按创建时间倒序翻页,Fire 带运行现在的状态和推送记录;删除任务连同触发历史和推送记录,发起过的运行留着
2026-10-05 08:43:44 +08:00
ThearchyHelios a2a19df9df 定时任务后台:三张表、输入模板、调度循环和 Webhook 推送,设置加「通知」一组
- 表:schedules(Webhook 地址和签名密钥加密存放、next_run_at 存 UTC、version 乐观锁)、schedule_fires((任务,
  fire_at, kind) 唯一)、webhook_deliveries;新表由 create_all 建,不用迁移
- 输入模板:{{ schedule.yesterday }} 这类变量按计划触发时间在任务时区算(不按「现在」:补发晚了、零点前后立即
  运行,「昨天」都是那次触发的昨天),上一次计划触发时间按 cron 往前找,同一次触发永远渲染出同一份输入;未知
  变量、未知过滤器、{{ }} 不成对都拦下,声明了入口字段的工作流还对照字段名和必填项
- 调度循环:每 15 秒一拍,条件 UPDATE 认领到点的任务并推进到下一次,两拍并发只有一拍认领得到,触发记录的唯一
  约束是第二道保险;上一次运行没结束(含停在审批、服务重启挂起的)记跳过;错过的只看最近一次,在宽限内补一次、
  超出记跳过,积压不补;发起走 launch_run,拒绝的原话记成未能发起,别的异常记通用说明、详情进日志;stop 等手上
  这一拍收尾,开关关着时 start 不起循环
- Webhook:通用 JSON(可选 HMAC 签名头)、飞书文本、企业微信 markdown、钉钉 markdown,按各平台算法签名;运行
  结束、失败、确有待审批时按订阅推一次,结束的等封存好再推,没订阅、没配 Webhook 的也记下已处理,之后配上不会
  补推旧的;10 秒超时、失败重试 3 次(4xx 不重试),每次推送记一条记录;通知里没有报告数字和原始报错,日志和
  错误里只有主机名(httpx 自己的请求日志也换成主机名)
- 设置 notify.public_base_url:通知里的运行链接 = 它 + /runs/{id},只收 http(s) 地址,末尾的 / 去掉
2026-10-05 08:37:57 +08:00
ThearchyHelios b51a686f53 定时任务 cron:自写五字段解析、下次 / 上次触发和中文描述,不加依赖
- 分 时 日 月 周五个字段,支持 *、列表、范围、步长和月份 / 星期的英文缩写,周日写 0 或 7;日、周都受限时取
  「或」,有一边以 * 开头就只看另一边(和 Linux 系统的 cron 一致,同一个表达式在这里和服务器上意思相同)
- 按任务时区的墙上时间匹配:夏令时跳过的本地时间那天不触发,拨回时重复的本地时间只在第一次出现时触发一次
- 写错的表达式给中文说明(哪个字段、允许的范围),所写日期在所写月份里不存在的(2 月 30 日)直接拦下
- 描述覆盖每天、每周、每月、每年、每 N 小时 / 分钟这些常见写法,说不清的写「按 cron 表达式 x」,后面带时区
- 时区名对照时区库换成规范名:不区分大小写的文件系统上 asia/shanghai 也打得开,换到 Linux 就打不开了
2026-10-05 08:27:35 +08:00
ThearchyHelios 26595b70f7 合入期 1 数字推演沙盘:证据面板里改口径卡的输入、排除查询结果的行,即时看指标和报告引用处的新值;推演只读、不写运行记录,醒目标注推演非出具;口径卡的容器型输入此后另存完整值
# Conflicts:
#	scripts/check-all.mjs
2026-10-05 07:53:42 +08:00
ThearchyHelios c8d0e5d18b 数字推演复核修正(第 3 轮):决定取哪一格的容器不再给可勾的行段,包着它的引用给了新值时不再说不准
- cell(nodes.q, len(vars.res.rows) - 5, …) 里的 vars.res.rows 这种决定取哪一格的数组字段、整份结果,第 2 轮起排除它的行
  只回「说不准」,可 row_span 照给:界面让人勾 10 行,勾了推演不出来,「不支持推演的输入」里又说它不能用。现在这种容器
  不给行段;决定取哪一格的单格照给(勾掉它确定是排除后无法计算)。前端 row_span 的类型说明跟着补上这一条
- 第 2 轮加的「说不准」不看包着它的引用有没有给新值:那一格改成 100、再排除别的行,以前能算出 100,第 2 轮起回说不准。
  _refs 现在记下每个嵌在里面的引用归哪个最外层引用(嵌几层都记最外层);最外层引用都给了新值时,只嵌在里面的输入无关
  紧要(_Plan.moot):它那一行排除了不再报排除后无法计算,对不上快照的也不再说不准。它自己也是最外层引用的照常拿值,
  只是取哪一格不再说不准
- 补测试:选取用的容器不给行段、只有那一格那一行勾了有确定结果(单元和真跑);那一格给了新值后排除任何行都照常算;
  既数行数又选取的容器;嵌在下标里的单格、对不上快照的下标(含嵌两层)、两个引用共用一个下标要都给新值;碰到了却没变
  时 maybe_changed 为假、下游口径卡不标(单元和真跑);嵌在下标里的遮罩格让整个指标推演不了(BLOCKED 加一例);读不出来
  的快照上嵌在里面的那一格不给行段(原先守它的测试被这次的选取判断盖住了)
- 逐个做过变异,全部变红:选取容器给行段、moot 不过滤、说不准不看 settled、_doubt 不看 moot、只记直接上一层、settled
  用 any、moot 含最外层引用,以及复核点名的 maybe_changed 只看碰没碰到、遮罩只看最外层。第 2 轮的两份变异单重放后也都
  变红,只有「row_span 只看快照读不读得出」成了等价变异:完整值找不回来的容器在最外层时整个指标推演不了,嵌在里面时
  又是选取用的,两头都已不给行段
2026-10-05 07:37:02 +08:00
ThearchyHelios bfc0e88e9a 数字推演复核修正(第 2 轮,后端):整个指标推演不了的不改动也照实说,完整值找不回来的数组排除行后不再报「无变化」
- 用到了遮罩列、最外层输入还原不了(容器的完整值找不回来、所在的查询快照读不出来)、卡上没记全输入的指标,以前不改动时
  报 unchanged(界面写「无变化」),输入照样可改、行照样可勾,改了、勾了才说推演不了。现在不论有没有改动都交回
  unsupported / masked 和原因,所有输入 editable 为假、不给 row_span,unsupported 里另有一条 input 为空的(界面据此只写
  原因),之后只列卡住它的输入。同一格既遮着、快照又读不出来时说遮罩
- 下游口径卡改看重算新给的 maybe_changed(改动碰到了某个指标,它又变了或说不准):推演不了的指标 changed 一律为空,
  只看 changed 分不出「什么都没改」,一打开就会把下游标成可能受影响
- agent 数组字段的完整值找不回来(节点产出不在、被改过,另存的完整值读不出来)时,排除它行段里的行以前因为值为空被忽略,
  指标报「无变化」、报告引用处报「不受影响」。现在算碰到了它(excluded_rows 照记,输入 changed 为真、new 为空),
  完整值找不回来的输入一律不给行段
- 决定取哪一格的容器(cell(nodes.q, len(vars.res.rows) - 1, …) 里的 vars.res.rows)少了行时,以前拿卡上记的那一格算出
  「无变化」,现在说不准(unsupported)
- 补测试:四类整个推演不了的指标不改动、改动后都照实说(单元和真跑);数组完整值找不回来时排除行段内一行(单元和真跑,
  含报告引用处);查询当时记下、跑完又从数据源去掉的遮罩(推演和勾行接口);写输入的节点和快照在同一条事件里交回
  (合并查询的边界);给了新值的输入不再说不准;数组只认行段内被排除的行;同一份快照交回两次按第一次;读得出来却不是
  查询结果的快照;嵌在里面、读不出来的那一格不进代入式。逐个做过变异:复核列出的四处、第 1 轮漏掉的三处和这次新加的
  判断改掉后都变红
2026-10-05 06:49:33 +08:00
ThearchyHelios 007a13d72b SQL 语义复核修正(第 5 轮):读同一张表的派生来源不再透成一张表、相关子查询分内外层、条件分组连接聚合按层计数,去重、UNION、最后出几行进比较
上一轮(3e0ab05)之后复验清单里还有一批用例返回空列表(A24 只报了一半)。最初的问题 1–3(paid / refunded 两个 CTE 换用哪一个的漏报、多一个 vip CTE 时配错、内连接换先后报出自相矛盾的两句)在第 3、4 轮已经修好,这次仍失败的是另外几类,根因各不相同:

读同一张表的几个派生来源:两个原样透出的 CTE(已付、未付,SELECT * FROM orders WHERE …)透过去,p.amount、o.amount 都成了 orders.amount,SUM、WHERE、时间列、ORDER BY、GROUP BY、关联条件、派生列的定义里用的是哪一份都分不出来(CTE 链、子查询套子查询同样);CTE 和它自己读的 orders 关联(父单、子单)也一样;同一个 CTE 关联两次时 _roles 设的 role 透过去就丢了。现在算完一段的来源后看各来源一路透过「只读一个来源」的派生来源最后读的是谁(_base),同一段里有几个读到同一个来源的,其中的派生来源标 opaque(_seal):through、时间列都不透过去,照写两边按结构配好的共同编号(已付配已付、未付配未付),给人看写名字(paid.amount),单看一条语句「只用到一张实表」时也照样写出是哪一份。时间条件给调用方对照数据目录的 detail 另一直换到实表(TimePredicate.table)。只有一个来源读那张表时照旧透过去。

相关子查询读外层那张表:EXISTS、标量子查询、LATERAL 里 c.parent_id = o.id 和 c.id = o.parent_id 都写成 orders.id = orders.parent_id(有子单还是有父单、之前还是之后有过订单分不出)。现在比较用的写法里外层的列在列名后面带「^」(_depth,往外几层带几个;带在列名上,排比较两边的先后只在两边表名一样时才受影响),给人看时内外读同一个来源的照写别名、留着表别名;两边各剩一个 EXISTS(NOT EXISTS)时配成一对说。

按整条语句去重:各段的条件、分组键、连接、聚合原来去重成一个集合,外层的 status = 'paid' 和 IN / EXISTS 子查询、关联进来的汇总 CTE、UNION 另一支里的同一个条件合成了一个,外层少了它报不出来(分组键、连接、聚合同理;聚合去重后只剩「多算了 SUM」一句,算出来的列又被压掉)。现在每一项记下在哪一层(Part / JoinFacet / Aggregate 的 scope,见 _layers:最外层、UNION 第几支、派生来源的共同编号、条件子查询「?」;只筛行的 CTE 和用到它的那一段同层,读同一张表的几个同段 CTE 各算各的层),按(层, key)去重;比的时候同一项在一边出现在几层算几个(_Compare.leftovers):同一层的先配,剩下的不管层也配(条件挪一层常是同一个口径),配不完的才报,另一边也有、只是层少了的说清是哪一层(「原流程在外层也只保留 status = 'paid' 的行,盲算在外层没有这个条件」)。条件子查询那一层多出来的不单说:外层那个条件已经报了,原来还会多出一句读起来像外层也这么筛的「只保留 state = 'done' 的行」。时间条件在列、范围都一样时也按层数比。

去重、合并、最后出几行:DISTINCT、UNION / UNION ALL 原来不进任何一面,分组键挪一层也看不出来。SELECT DISTINCT(这一段没有分组、聚合时)当这一段的分组键收:和按同几列 GROUP BY 一样的不报,一边在子查询里先去重报「去重」;各段的 UNION / INTERSECT / EXCEPT 记下合并方式和支数(SetOpFacet,同层的先配,挪了层的同样写法也配上);最后出来的行按什么分(Facets.grain:分组键 / 只出一行 / 逐行)在分组键都配得上、两边都汇总了时比(先在子查询里按用户分组、外层数用户数,对外层按用户分组)。

说清楚:自己关联自己、关联的方向反了,两边都写 a.amount 时说清各自的 a 是关联条件里出哪一列的那个(Part.roles,实表自连接同样)。

模块说明、Part / JoinFacet / TimePredicate / Aggregate / Facets / FacetDiff、unmatched / leftovers / joins / render 的文档按现在的做法改写,做不到的照实列出:读同一张表的几个派生来源同段时只认配好的编号(一边两个 CTE、另一边把其中一个换成关联实表再在 WHERE 里筛,算不同;同一个 CTE 关联两次对定义成两个一样的 CTE 也算不同);挪了层又多一层时挑哪一个说成多出来的有顺序约定;条件子查询那一层多出来的不单说;UNION 各支按位置配;不加表前缀引用外层的列认不出;LATERAL 里的相关条件两边各说一句;最后出几行只在两边都汇总时比;COUNT(DISTINCT x) 对数 DISTINCT 子查询行数照报不同(有空值时确实不一样)。

测试:复验清单里的用例逐个补成测试(三个方言,CTE 和子查询写法),另加意思一样、不该报的反例(关联换先后、改名、单个透出的 CTE、自连接换别名、相关子查询换别名、两边都两层、条件挪进汇总 CTE、分组写在 CTE 里、GROUP BY 对 DISTINCT、一样的 UNION、最外层 UNION 对包一层)。新加 132 项,其中 119 项在 3e0ab05 上失败,其余 13 项防误报的两边都通过;原有 192 项和 test_verify_agent 照旧通过,全量 7245 项通过。另拿测试里出现过的每条 SQL 在三个方言下和自己比(1051 次),都没有不同。复验清单最后一条只到「b_s」就截断了,看不到全文,按开头补了 DISTINCT 子查询对不去重、对 COUNT(DISTINCT) 的对照。
2026-10-05 06:11:40 +08:00
ThearchyHelios 09237a4c99 数字推演复核修正(第 1 轮,后端):排除行时说不准的指标不再报「无变化」,快照读不出来时不给值,遮罩按用到的快照在线程里算
- 排除了行,指标却有输入对不上任何快照(老运行没存完整值的容器、加工过的结果、按位置从整份结果里取的一格、由查询结果
  算出来的数):以前一律报「无变化」、报告引用处报「不受影响」,或者拿原值算出一个不对应任何一份数据的数。现在回
  unsupported 并写明是哪个输入说不准;写它的节点在那份快照交回之前就跑完的影响不到它,照常不算(按事件序号比先后)。
  卡上没记全输入的指标,有任何改动都标 unsupported
- 查询快照文件不在、内容与哈希不一致时,落在它上面的 cell()、agent 字段不给值、不能改、不给代入式,用到它的指标推演
  不了;遮罩照样按台账记的数据源算,不再因为快照坏了就失效
- 遮罩改成用到哪份快照才算哪份:读快照放进重算的工作线程,查库交回事件循环。以前每次请求都在事件循环里把这次运行的
  全部快照读一遍、复验一遍哈希
- 输入带 row_span(实际用到的快照行);新增 GET …/evidence/whatif/rows,按页取封存范围内一份快照的行(遮罩列写
  「已遮罩」,只读)。界面据此给整份结果勾行、只让用到的行可勾
- 不能改、也没有行可勾的输入(对不上快照的列表、文本)列进「不支持推演的输入」并写明原因
- 下游口径卡顺着画布的边找,经中间节点用到卡上值的也标;改动碰到了指标、它却推演不了时也标「可能受影响」
- 超大整数不再让接口 500:新值超出可计算范围回 422,算出浮点放不下的整数按「不是有限的数值」说明;非法值的说明不露出
  None、单引号这些 Python 写法
- 补测试并逐个做过变异:封存核对没过、事后加的遮罩、合并查询的遮罩、指标集被改(409)、两份报告、循环里先算卡后算
  产出、手写事件核对取哪次执行和边界、重算的几处保护、勾行接口。取整和口径卡逐个对拍(含二进制半中间的数、负小数位):
  metrics.py 只许加 value_artifact,取整没法抽成共用函数,先用对拍守着
2026-10-05 05:43:17 +08:00
ThearchyHelios e7c8d2a1f3 可验证报告复核修正(第 1 轮):运行信息拿已核对的事件对照,报告徽标连同数字一起算,遮罩判定宁严勿宽,各分支在浏览器里有确定的检查
- 页面核对加 ⑥ 运行信息:运行结果看封存范围内最后一条结束类事件的类型,运行类别看运行开始事件记下的运行参数,
  出具档位认最后一条出具判定(不再信元信息指的那一条);页面显示事件里的值,元信息写得不一样算不通过、计入总览。
  工作流名、版本、起止时间、节点名称绑不进封存,单列一组写明取自运行记录,封存说明里也写明要到系统里一并对照
- 报告标题旁的徽标连同正文里的数字一起算,旁边写分项:重算全部哈希的伪造只在 ⑤ 露馅,徽标不能还写着通过。
  分块条目多时不通过和无法核对的都列在外面,有无法核对的显示「部分无法核对」和分项计数;⑤ 改叫「数字与取值」并分开数,
  读屏分别读作数字、取值,「显示格式由服务端渲染」只附在带出处的数字上;无出处数字的说法改成未使用引用标记
- 服务端不会给出的状态算不通过:被引用的工件不在清单里、非查询快照标着遮罩、工件既没内容也没说明;报告文档导出时
  已不在服务端,总览不再是绿色的通过,写「有 N 份报告未能核对」,报告处写明是服务端已没有这份文档
- 认 json.dumps 写出的裸 NaN / Infinity,没被改过的文件不再误判不通过;哈希对得上但读不懂的照实写「无法核对」,
  和哈希不一致分开说
- 宽屏打开出处面板时正文让出右边一栏,表格里右对齐的数字不再被面板盖住;补上斜体(不用后行断言,较早的 Safari 也能执行)
- 封存哈希、运行 ID、工作流名直接写进页面,不执行脚本时写「尚未核对」而不是一直「正在核对」;脚本挪进 head,
  执行后核一遍页面顶部写出的封存哈希和数据里核对用的一致
- 成果节点和运行结束事件的规范字符串,只在封存范围内每次数据源查询都对得上无遮罩的快照(或确实没交回数据)时才给:
  升级前的运行从工具快照认出查询快照,协作团队成员的查询认不出就按带遮罩算,数据源现在设了遮罩的按工具名就挡
- 封存核对不一致的 409 改成一句固定的话,不再叠冒号、让人去本来就在的记录页;失败结束的运行在页面封存说明和导出提示里
  写明之后继续运行会重新封存;记录页上左边写着「已封存」却不能导出时,原因写在屏幕上
- tests/bundle_samples.py 用虚构数据真实生成九份样本页(遮罩快照、两份报告、早期运行、失败结束、两条出具判定、
  服务端缺文件、裸 NaN、别的运行的文档),check-bundle 断网打开逐项断言,在上面篡改引用链、运行信息、拿掉被引用的口径卡,
  量宽屏面板,看不执行脚本的样子;把核对改成恒通过、徽标不看数字等七种改坏法都会变红。pytest 补遮罩判定的三种形状、
  事后设遮罩(去掉接线即变红)、页面静态内容和样本页本身
2026-10-05 05:30:50 +08:00
ThearchyHelios 3e0ab059b1 SQL 语义复核修正(第 4 轮):外连接按主表比、算出来的列按定义比、时间列透过 CTE 认、筛的先后照报、没用到的 CTE 不算,同名却不是同一个的派生来源说清各是什么
上一轮(db91c9e)给派生来源加了结构标识、按结构配对之后,复验还有一批用例返回空列表或者报得自相矛盾。按根因分几类改:

外连接:joins 第二遍按涉及的来源配对、不分方向,orders LEFT JOIN refunds 和 refunds LEFT JOIN orders 配成一对后类型、条件都一样,一句不说(db91c9e 引入的回归),RIGHT JOIN 对调、两个 CTE 对调、子查询写法同样漏。现在 JoinFacet 记下主表(keep)和可空那一侧(nullable),LEFT / RIGHT 按主表比:对调了报「原流程以 orders 为主表外连接 refunds,盲算以 refunds 为主表外连接 orders」,LEFT JOIN 和两边对调的 RIGHT JOIN 是同一回事、不再报成类型不同(原来列为做不到)。同一段 WHERE 里拿可空那一侧的列比较、IN、LIKE、BETWEEN、IS NOT NULL 的(JoinFacet.rejects),这次外连接按内连接比:盲算把可空一侧的条件写进 WHERE、LEFT JOIN 实际成了内连接的照报;IS NULL、OR、套在 COALESCE / CASE 里的不认。

算出来的列(Computed,新的 expression 一面):聚合只按去重后的 (函数, 参数) 集合比,SUM(p.total) - SUM(r.total) 对反过来、退款率分子分母对调报不出;派生来源里不是聚合的列不进任何方面,net.v 谁减谁、x.total 取的是付款还是退款合计、amount * 0.9 对 0.8、CASE 的口径、窗口函数的分区和排序、RANK 对 ROW_NUMBER 都是空列表。现在最外层选出来的列(先剥掉 ROUND、CAST、COALESCE 给默认值这类外壳)按算法比,用到的聚合一样、位置换了照报;派生来源里被外面用到、又不是原样透出一列的列按定义比。只差在聚合上、聚合那一面已经报了的,按透过去的那一列筛时间、时间那一面已经报了的,不再报第二遍。比较时加法、乘法的两项排好先后,DATE()、CAST(… AS DATE)、::date 统一成一种写法,免得写法不同被报成算法不同。

时间:时间条件的列透过「同一列套着日期函数、CAST、MIN / MAX」算出来的派生列,一直换到实表(TimePredicate.via 记下路过的列):日期表达式写在 CTE 里,按支付时间还是下单时间筛照样报出来,带上表名、列名给调用方对照数据目录(三个方言的 DATE()、CAST … BETWEEN、两个同名 CTE 对调后的 MAX(paid_at) 对 MAX(refunded_at))。

结构标识和配对:_Derived 里往里套着的派生来源原来一律写「@」,net 是 paid.total - refunded.total 还是反过来写出来一样。现在按结构写的 shape 写法里引用派生来源的地方写它自己的结构标识(里面那段按 shape 写出来再取摘要),配对之前比较的两边也按结构标识排先后;配对之后仍按共同编号排(配成一对的两个派生来源里面稍有不同时结构标识就不同,按它排会把同一个比较排成两个方向)。_align 同一级先看条件重合、再看名字:两边都叫 paid、一边筛已付一边筛未付时不再按名字配反。配成一对的派生来源里两边起名不同的列按定义配成同一列(total 对 sum_amt,原来列为做不到;_renamed),两边各剩一列时也配上,定义的不同照报。

筛的先后:派生来源取前几行(LIMIT、OFFSET)或算了窗口函数(这一列又不是分区键,含 DISTINCT ON)时,外面的条件不再透成里面那张表的条件;Part 另记一路透到底的写法(pierce)和挡住它的是什么(stage),两边只差在这里时报「原流程先算 t 的窗口函数、再按 status = 'paid' 筛选,盲算先按 status = 'paid' 筛选、再算 t 的窗口函数」「先取 t 的前 10 行、再筛」,时间条件同样。

没用到的 CTE:原来逐段收集连定义了没用的 CTE 里的条件也收,盲算定义了只筛已付的 CTE 却直接查 orders 报不出来,没用的 CTE 里多一个条件又会被报成多筛了一遍。现在只收从最外层一路引用到的段(_Statement._enliven);例外是两边都定义了同一个 CTE、一边把它关联进来一边没用到,没用到的那边照样收它,免得它里面的聚合、分组被说成一边独有(连接那一面已经说了关联了它)。

说清楚:两边同名的派生来源其实不是同一个时(t1 / t2、a / b 定义对调),「按 t1.total > 100 筛选」两边写法一模一样,读起来自相矛盾。Part 记下用到的派生来源各列的来历(origins,一直换到实表的列),给人看的写法依次退到一律写表名、带上来历(t1.total 即 SUM(orders.amount))、来历也一样(两个 CTE 只差里面的条件)时说「盲算的 paid 对应原流程的 open」。

自己关联自己:同一个 CTE、同一张表在一段里出现两次,两个别名按各自在关联条件里出的列区分(_Source.role),b1.total > b2.total 写反了报得出来,换了别名、换了关联先后不报(原来列为做不到)。CTE、子查询自己写了列名的(WITH t(n) AS …,递归 CTE 常这么写)按写的列名认列。

模块说明、Part / JoinFacet / TimePredicate / Computed / Facets / FacetDiff、unmatched、joins 的文档按现在的做法改写,做不到的照实列出:两边起名不同、各多出不止一列且定义都不同的列仍算两列;全换了的派生来源不配对;条件改得一个都不重合又各起了名字的同形派生来源只能按先后配;自连接两个别名在关联条件里出的列一样时分不出;三项以上的加法乘法换序、括号展开算不同算法;QUALIFY、GROUP BY 之后才筛的先后不认;外连接链里连带变成内连接的、套在函数里或写在外层 WHERE 的不认;不是 facets 算出来的 Facets 只能按位置编号比。

测试:复验清单里的用例逐个补成测试(各方言、CTE 和子查询写法),另加意思一样、不该报的反例(LEFT / RIGHT 对调、WHERE 筛主表、外连接链换序、0.9 * amount、DATE 对 CAST、按分区键在窗口前后筛、两边都先取前 10 行、列起名不同、自连接换别名换先后、没用到的 CTE)。新加 64 项,其中 48 项在 db91c9e 上失败,其余是防误报的,两边都通过;原有 128 项和 test_verify_agent 照旧通过,全量 7113 项通过。复验清单最后一条只到「WITH base AS (SE」就截断了,看不到全文,按开头和已知缺口一并处理了 CTE 自连接。
2026-10-05 04:58:17 +08:00