Files
AgentLab/scripts/ledger-fixtures.mjs
ThearchyHelios 271bc6e782 口径标签逐格判:引用了好几列的查询,每个数按它自己那一格标
真实模型下问「上周的销售额是多少」,助手写 SUM(amount) AS 销售额, COUNT(*) AS 订单数, MIN / MAX(paid_date) AS 区间首日 /
末日,报告四列都引用了,口径「周销售」只取 SUM(amount)。按「全部引用列」投影后还是 4 列,对不上口径的 1 列,整条比、
四个数都标相关口径,说明整段兜底。

- ledger/match._cells / _cell(algo 4):整条(含按全部引用列投影)没证明、又引用了这条查询两列以上的,被引用的每一列单独
  投影到那一列,和每份口径的每个指标读的那一列(card_reads;口径版本没有口径卡的按取数的每一列)逐个比,仍只走
  match_template / explain_sql_diff,上次的投影规则照用:投影不了(分组列、DISTINCT……)、过了时限、不可比的格照用整条
  查询的那一个候选。结果记在 queries[].cells {列名: {status, candidates}},另记 one_row(sql_semantics.one_row:不分组的
  聚合,一定只出一行)。整条这一层的结论、候选一字不改;老工件没有 cells,照旧按整条读。
- ledger/labels:单元格按它那一格的结论标(_view);指标用了同一条查询的好几格时,只有那条查询一定只出一行才把逐格的
  结论合起来用(多行的结果里每一列各自对得上,不等于每一行的几列一起对得上)。整题照拍板 L1 不变:每个数都证明来自
  同一份口径才等价;一格证明了、别的格只是相关的整题不等价,原因写「有的数和相关口径不一致,不来自口径」(以前一律说
  「这次的推理」)。出具入口照 ledger/issue 现有判法只在整题等价时给,没有放宽。
- 逐格证明了的口径各处都算上(labels.candidates_of):工件 meta 的 calibers(召回按旧版本反查)、ledger/uses 的使用记录、
  召回时找原答案对应的那一格、E4;E1 另数各种结论各几格(cells),E2 的兜底比例按标签上给人看的那一份说明数。
- GET /runs/{id}/ledger 的 queries 带上逐格的 cells(遮罩同候选);证据片段的 ledger 键、轮次摘要的计数本来就按每个数的
  标签,数据变了、形状不变。前端只补类型;check-ledger 加一段:同一条查询一格来自口径(正文挂小标记)、一格相关(差在哪
  先说多查了几列),整题不等价不给出具。
- 评测逐题比 SQL(eval/score.sql_match)取和期望配上的那一格(score.matched_cells:评分时配上的格),没给时取逐格证明了
  的那一格;结果带 column。runner、rescore 传它。

测试:tests/test_ledger_cells.py(同一条查询一列等价、一列不等价各自标对;两份口径只证明了一份标那一份;口径多个指标按列
配对;指标跨两格只在一行的查询上用逐格结论;分组列、DISTINCT 逐格也不投影;照真实例子造的虚构库真跑一遍:销售额标等价
已证明,订单数、首末日标相关,整题不等价、不给出具、不自动出具,证据片段的 ledger 键一致;评测取配上的那一格)。去掉逐格
再跑,10 条失败;去掉「一行才合起来用」、去掉评测取格,各 1 条失败。
2026-10-17 13:07:19 +08:00

177 lines
13 KiB
JavaScript
Raw Permalink 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.
// 口径账本的检查夹具(check-ledger、check-evidence、check-chat 共用):GET /api/runs/{id}/ledger 的答复形状照服务端
// app/ledger/view.py 的 run_view(labels.view_label、issue.Plan.action、reconcile.actions_of / reconciled_view),
// 挂在可点击证据的夹具(frontend/src/run/__tests__/evidence-doc.json,compose_doc 真跑出来的周报,只用通用名)上:
// s6「45,678.5元」来自受管口径「周报口径」v2,等价已证明;s10「1,234单」来自已发布口径「订单口径」v1,助手选用;
// s8「8.7%」相关口径「订单口径」v1(差在哪:少了一个筛选条件),可以对账;s12「37.0元」这次的推理;
// s23「2.35%」没有比对(比对超时)。
// 口径名、表名、数都是虚构的通用名。
/** 口径的几份(calibers) */
export const WEEKLY = { caliber: 'wf-weekly@2', workflow_id: 'wf-weekly', version: 2, level: 'governed', level_text: '受管',
workflow_name: '周报口径', graph_hash: 'aa11' }
export const ORDERS = { caliber: 'wf-orders@1', workflow_id: 'wf-orders', version: 1, level: 'published', level_text: '已发布',
workflow_name: '订单口径', graph_hash: 'bb22' }
const caliberLabel = (c, how, extra = {}) => ({
kind: 'caliber', how, caliber: c.caliber, workflow_id: c.workflow_id, version: c.version, level: c.level,
level_text: c.level_text, workflow_name: c.workflow_name, ...extra,
})
export const DIFF = { items: [{ aspect: 'filter', a: null, b: "status = '[已遮罩]'", text: '口径只保留状态为已支付的行,这次没有这个条件' }],
fallback: false, text_diff: [] }
/** 一份比对过的账本。whole:整题判断(缺省不等价);actions、issued、reconciled 可以换 */
export function ledgerOf(runId, { whole, actions, issued = [], reconciled = [], retro = false, labels } = {}) {
return {
run_id: runId, state: 'matched', note: '封存后比对,不在封存范围内', post_seal: true, artifact: 'cm-1', algo: 'v1',
trigger: retro ? 'on_demand' : 'sealed', retro, matched_at: '2026-10-15T01:00:00+00:00',
...(retro ? { retro_note: '按现在的口径回看' } : {}),
calibers: [WEEKLY, ORDERS],
queries: [
{ artifact: 'q-gmv', node_id: 'ag', source: 'shop', tables: ['orders'], status: 'proven',
candidates: [{ ...WEEKLY, node_id: 'fetch', status: 'proven', how: 'independent' }] },
{ artifact: 'q-orders', node_id: 'ag', source: 'shop', tables: ['orders'], status: 'related',
candidates: [{ ...ORDERS, node_id: 'fetch', status: 'related', comparable: true, via: 'heuristic', diff: DIFF,
binding: [{ field: 'start', value: '2026-09-01' }] }] },
],
reports: { write: { labels: labels ?? {
s6: caliberLabel(WEEKLY, 'independent', { binding: [{ field: 'week', value: '2026-W37' }], card: 'caliber', metric: 'gmv',
queries: ['q-gmv'] }),
s10: caliberLabel(ORDERS, 'composed', { binding: [{ field: 'store', masked: true, value: '已遮罩' }], card: 'caliber',
metric: 'orders', queries: ['q-gmv'] }),
s8: { kind: 'related', related: [{ caliber: ORDERS.caliber, workflow_id: ORDERS.workflow_id, version: 1, level: 'published',
level_text: '已发布', workflow_name: '订单口径', node_id: 'fetch' }], queries: ['q-orders'] },
s12: { kind: 'reasoning', query: 'q-gmv' },
s23: { kind: 'unknown', reason: '比对超过时限,没有做完', query: 'q-gmv' },
} } },
primary: 'write',
whole: whole ?? { equivalent: false, reason: '有的数是这次的推理', counts: { caliber: 2, related: 1, reasoning: 1, unknown: 1 } },
actions: actions ?? [RECONCILE],
issued, reconciled,
}
}
/**
* 答案身上的召回标记(服务端 ledger/recall.answer_marks):周报口径升到 v3 重算过、数没变(同一份口径核对过两次,只写最近
* 那一版);目录里影响数字的两项被驳回、按新目录检查新出现 1 条错误级问题,另有一次出现了新码值(同归「数据目录」那一类,
* 项数相加);看的这个人手上有一条说的是这个答案的 recall 待办(订单口径升到 v2,数变了)
*/
export const RECALL = {
checked: [
{ workflow_id: 'wf-weekly', workflow_name: '周报口径', from_version: 1, to_version: 2, recall_run: 'run-rc0', changed: false,
at: '2026-10-14T01:00:00+00:00' },
{ workflow_id: 'wf-weekly', workflow_name: '周报口径', from_version: 2, to_version: 3, recall_run: 'run-rc1', changed: false,
at: '2026-10-15T01:00:00+00:00' },
],
basis: [
{ kind: 'catalog', items: 2, new_errors: 1, at: '2026-10-15T02:00:00+00:00' },
{ kind: 'values', items: 1, new_errors: 0, at: '2026-10-15T03:00:00+00:00' },
],
tasks: [{ id: 'tk-recall-1', reason: 'caliber', to_version: 2 }],
}
/** 看差多少(s8 那条查询、订单口径):参数是推断的,要先核对 */
export const RECONCILE = {
kind: 'reconcile', query: 'q-orders', caliber: ORDERS.caliber, workflow_id: ORDERS.workflow_id, version: 1, node_id: 'fetch',
workflow_name: '订单口径', level_text: '已发布', needs_check: true, check_reason: '参数按这次的 SQL 推断,请核对',
binding: [{ field: 'start', value: '2026-09-01' }], fallback: false,
fields: [{ name: 'start', required: true, has_default: false }, { name: 'store', required: false, has_default: true }],
}
/** 按口径正式出具(整题等价于周报口径):搭建者能一键,不自动的原因写在 auto_reason */
export const ISSUE = {
kind: 'issue', workflow_id: WEEKLY.workflow_id, version: 2, caliber: WEEKLY.caliber, workflow_name: '周报口径', how: 'independent',
level: 'governed', level_text: '受管', available: true, auto: false, auto_reason: '设置里关了自动按口径出具',
}
/** 整题等价时的标签(三个数都来自周报口径) */
export const WHOLE_LABELS = {
s6: caliberLabel(WEEKLY, 'independent', { binding: [{ field: 'week', value: '2026-W37' }], queries: ['q-gmv'] }),
s10: caliberLabel(WEEKLY, 'independent', { binding: [{ field: 'week', value: '2026-W37' }], queries: ['q-gmv'] }),
s12: caliberLabel(WEEKLY, 'independent', { binding: [{ field: 'week', value: '2026-W37' }], queries: ['q-gmv'] }),
}
export const WHOLE = { equivalent: true, caliber: WEEKLY.caliber, workflow_id: WEEKLY.workflow_id, version: 2, level: 'governed',
level_text: '受管', workflow_name: '周报口径', how: 'independent', binding: [{ field: 'week', value: '2026-W37' }],
missing: [], missing_required: [], counts: { caliber: 3, related: 0, reasoning: 0, unknown: 0 } }
/**
* 逐格判(服务端 ledger/match._cells,algo 4)的真实例子:同一条查询(q-gmv)报告引用了好几列,s6 销售额那一格证明了
* 等价于周报口径,s10 订单数那一格只是相关(差在哪先说多查了几列、再说计数对求和),整题不等价、没有出具入口
*/
export const CELL_DIFF = { items: [
{ aspect: 'projection', a: null, b: null, text: '这次多查了 3 列,不影响这个数' },
{ aspect: 'aggregate', a: 'COUNT(*)', b: 'SUM(amount)', text: '这次数的是行数,口径对 amount 求和' }],
fallback: false, text_diff: [] }
export const CELL_LABELS = {
s6: caliberLabel(WEEKLY, 'independent', { binding: [{ field: 'start', value: '2026-10-05' }, { field: 'end', value: '2026-10-11' }],
query: 'q-gmv', node_id: 'fetch', column: 'gmv' }),
s10: { kind: 'related', query: 'q-gmv', related: [{ caliber: WEEKLY.caliber, workflow_id: WEEKLY.workflow_id, version: 2,
level: 'governed', level_text: '受管', workflow_name: '周报口径',
node_id: 'fetch', comparable: true, diff: CELL_DIFF }] },
}
export const CELL_WHOLE = { equivalent: false, reason: '有的数和相关口径不一致,不来自口径',
counts: { caliber: 1, related: 1, reasoning: 0, unknown: 0 } }
/** 对过的一次账(单行:一组对照)。status 可以换 */
export const reconciledOf = (status = 'succeeded') => ({
run_id: 'run-rec-1', status, query: 'q-orders', caliber: ORDERS.caliber, workflow_id: ORDERS.workflow_id, version: 1,
inputs_from: 'checked', rows: null, card: 'reconcile',
groups: [{ label: '订单数', ours: 'ours_0', caliber: 'cal_0', diff: 'diff_0', rel: 'rel_0' }], created_at: '2026-10-15T02:00:00Z',
})
/** 对账卡上四个指标的展示值(/evidence/metrics/reconcile/{id} 的 rendered) */
export const RECONCILE_VALUES = { ours_0: '1,234单', cal_0: '1,180单', diff_0: '54单', rel_0: '4.58%' }
/** 对账运行的指标出处(形状同片段接口:chain、seal),给 EvidenceDrawer 画 */
export const metricDetail = (runId, metric) => ({
run_id: runId, node: 'reconcile', metric, rendered: RECONCILE_VALUES[metric] ?? '—', eid: `ev:metric:${metric}`,
chain: [{ step: 'metric', alias: 'm', metric, name: metric, value: 1, rendered: RECONCILE_VALUES[metric], node_id: 'reconcile',
caliber: '口径对账', version: 'v1', expression: 'cell(nodes.ours_q1, 0, \'orders\')', sealed: true, inputs: [] }],
seal: { sealed: true, ok: true, covered: true }, redacted: { columns: [] },
})
/**
* 对账卡上「按口径」那个数的出处(服务端 api/evidence._upstream_steps 的形状):它转抄口径卡的指标
* (nodes.card.metrics[0].value),出处跨卡接上口径卡「订单数」那一步,再到口径的取数查询(q-cal-fetch)
*/
export const copiedMetricDetail = (runId, metric) => ({
run_id: runId, node: 'reconcile', metric, rendered: RECONCILE_VALUES[metric] ?? '—', eid: `ev:metric:${metric}`,
chain: [
{ step: 'metric', alias: 'm', metric, name: '订单数 · 口径', value: 1180, rendered: RECONCILE_VALUES[metric], node_id: 'reconcile',
caliber: '口径对账', version: 'v1', expression: 'nodes.card.metrics[0].value', substituted: '1180', recompute_ok: true,
sealed: true, hash_ok: true, untraced: [],
inputs: [{ path: 'nodes.card.metrics[0].value', value: 1180, node_id: 'card', via: 'metric', field: 'metrics[0].value',
status: 'ok', upstream: { metric: 'orders', artifact: 'ms-card', depth: 1, traced: true } }] },
{ step: 'upstream', of: 'nodes.card.metrics[0].value', node_id: 'card', label: '口径卡', artifact: 'ms-card', metric: 'orders',
traced: true, depth: 1, hash_ok: true, sealed: true, name: '订单数', value: 1180, unit: '单', rendered: '1,180单',
caliber: '订单口径', version: 'v1', expression: "cell(nodes.fetch, 0, 'orders')", substituted: '1180', recompute_ok: true,
status: 'ok', untraced: [],
inputs: [{ step: 'input', path: "cell(nodes.fetch, 0, 'orders')", value: 1180, node_id: 'fetch', via: 'tool_cell',
status: 'verified', artifact: 'q-cal-fetch', locator: { row: 0, column: 'orders' }, eid: 'ev:cell:q-cal-fetch' }] },
{ step: 'query', alias: null, artifact: 'q-cal-fetch', node_id: 'fetch', tool: 'db_query__shop', source: 'shop',
sql: "SELECT COUNT(*) AS orders FROM orders WHERE status = 'paid'", columns: ['orders'], rows: [[1180]], row_offset: 0,
row_index: [0], total_rows: 1, highlight: { rows: [0], cols: ['orders'], cells: [[0, 'orders']] }, sealed: true, hash_ok: true },
],
seal: { sealed: true, ok: true, covered: true }, redacted: { columns: [] },
})
/** 认证队列:三类来源各一项 */
export const QUEUE = {
items: [
{ id: 'shape:aa11', kind: 'shape', source: 'shop', shape: 'SELECT SUM(amount) FROM orders WHERE paid_at >= ? AND status = ?',
runs: 4, conversations: 3, question: '上周销售额是多少',
turns: [{ run_id: 'run-s1', conversation_id: 'c-1', turn_id: 't-1', question: '上周销售额是多少' },
{ run_id: 'run-s2', conversation_id: 'c-2', turn_id: 't-2', question: '这周销售额是多少' }],
params: [{ index: 0, distinct: 3 }], constants: 1, related: [{ caliber: ORDERS.caliber, workflow_id: ORDERS.workflow_id, version: 1, level: 'published' }],
representative: { run_id: 'run-s1', query: 'q-1' }, peers: ['run-s2', 'run-s3'], frozen_into: [] },
{ id: 'cluster:bb22', kind: 'cluster', question: '各门店的退款率', count: 3,
turns: [{ run_id: 'run-c1', turn_id: 't-c1', question: '各门店的退款率' }, { run_id: 'run-c2', turn_id: 't-c2', question: '门店退款率怎么样' }],
frozen_into: [] },
{ id: 'reused:cc33', kind: 'reused', question: '本月新客数', uses: { exports: 2, app: false, extracted: false, canvas: true },
turns: [{ run_id: 'run-r1', conversation_id: 'c-3', turn_id: 't-r1', question: '本月新客数' }],
representative: { run_id: 'run-r1' }, frozen_into: [{ workflow_id: 'wf-new-old', name: '本月新客数(固化的口径草稿)', status: 'draft' }] },
],
considered: { runs: 120, matched: 100, questions: 90 },
threshold: { runs: 3, conversations: 2, exports: 2 },
}