fix(consult): check the evidence gate against the route the answer is on
Independent Staging Quality Gate / validate (push) Successful in 7m41s
Independent Staging Quality Gate / publish (push) Successful in 9m39s

七条路由的证据门都不是自己的:route_requirements 的键写成 relationship/finance,而
路由名是 marriage/wealth,另有 5 条路由压根没有条目,全部静默落到 general 的门。
missingLayers: [] 因此不表示证据齐备,只表示没检查过——婚姻的 UL 与财富的 D2 从未
进入检查。

同一函数另有两处判据也没接到权威来源。7 块正则用问题文本重猜领域,而领域早已由模型
声明并写进 route_packet,一句写作「情感」而非表里「感情」的提问在 marriage 路由上完全
拿不到性别解读边界。timing_layers_ready 读的是 missing_route_layers,该列表只装本路由
要求的层,于是对任何不要求 narayana_dasha 的路由恒为真,精确应期在该层根本没算出来时
也照样放行。三处的失败方向都是静默放宽,因此没有任何人报错。

三处都接回权威来源:10 条路由逐条显式列出必需层(层名限定为证据包真实构建的 section,
所以 wealth 不要求引擎不产出的 D11)、领域边界按 route 查表、出生时间边界从矫正闸门的
effective_accuracy 与 Lagna 敏感度派生、就绪判断直接读 section 状态。唯一保留文本探测
的是「用户有没有要一个具体日期」——服务端对此没有权威来源,改为 timing/annual 路由结构
性携带、文本仅作叠加,一次措辞漏判不再能把信号清零。

另外把 skillReferenceReadCount 暴露为回执的 skill.referenceReads(必填)与可观测日志的
skillReferenceReads。它此前数完即丢,而 skill_read 按设计不记成 runtime step,因此「模型
有没有真的翻开方法文档」在运行结束后无处可查。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-18 11:50:28 +08:00
parent 5687182980
commit 10ae149c58
9 changed files with 347 additions and 46 deletions
+34
View File
@@ -3941,3 +3941,37 @@
- 相关记录:ERR-103`docs/research/pre_work_error_ledger.md`,同一 Compose 现象的误诊,本次给出真实根因)、ERR-105(同一台跳板机磁盘耗尽的基础设施记录)、BUG-264(本次被卡住无法发布的修复)
- 复发自:无
- 修复版本:本地未提交候选
## BUG-267 | 咨询证据门的三处判据都没接到权威来源:婚姻/财富的必需层从未被检查,领域边界靠猜关键词,精确应期无条件放行
- 状态:resolved(本地修复,待提交与发布)
- 首次发现:2026-08-18
- 最近更新:2026-08-18
- 影响面:`scripts/jyotish_api_server.py``_build_consumer_context()`,即所有产品咨询(web `/api/consult`)与 MCP/研究路径共用的回答真相合同。直接影响 10 条路由中 7 条的证据门、6 个领域的解读边界,以及全部路由的精确应期许可。
- 用户现象:手测 staging 时,一次婚姻方向的咨询回执报 `status: "ready"``missingLayers: []``preciseTiming: "allowed"`,看上去证据齐备;但同一次回答里既没有 Upapada(UL)层的任何结论,也没有性别解读边界。回执的「齐备」与回答的内容互相矛盾,而回执本身不含任何可据以追问的线索。
- 触发条件:任何走 `marriage``wealth``health``education``migration``family``annual` 路由的咨询(即除 `career``timing``general` 外的全部路由);以及任何问题文本没落在七条关键词正则里的领域提问。
- 根因:同一个函数里三处判据各自接错了来源。其一,`route_requirements` 的键写成 `relationship``finance`,而 `_ROUTE_DEFINITIONS` 里的路由名是 `marriage``wealth``.get(route, general)` 于是静默把这两条路由降级成 `general` 的门(`D1/D9/dasha_boundaries`),婚姻的 UL 与财富的 D2 从未进入检查;另有 5 条路由(health/education/migration/family/annual)压根没有条目,同样落到 `general``missingLayers: []` 因此不表示证据齐备,只表示没检查过。其二,7 个领域上下文层与解读边界由 7 块 `re.search` 匹配问题文本决定,而领域此时已经由模型声明并写进了 `route_packet`——用文本再猜一遍既是重复,又只能覆盖关键词表里的写法:本次实测中一句明显是情感取向的问题因为写作「情感」而非表里的「感情」,在 `marriage` 路由上完全没拿到性别解读边界。其三,`timing_layers_ready` 判断 `dasha_boundaries``narayana_dasha` 是否就绪时读的是 `missing_route_layers`,而该列表只包含**本路由要求的**层,于是对任何不要求 `narayana_dasha` 的路由恒为真,`can_answer_precise_timing` 在该层根本没算出来时也照样放行。三处的共同形态是:判据没有落在权威来源(路由表、路由声明、证据 section)上,而是落在错键、文本猜测和一个恰好为空的列表上,因此全部失败方向都是「静默放宽」。
- 修复:把三处判据都接回权威来源。`_ROUTE_REQUIRED_LAYERS` 提为模块级常量并对 `_ROUTE_DEFINITIONS` 的 10 条路由逐条显式列出,不再依赖 `general` 兜底;表里每个层名都限定为证据包真实构建的 section(因此 `wealth` 只要求 D2 而不要求引擎当前不产出的 D11,`migration` 要求确实产出的 D4),避免把状态整体推成 `degraded`。领域上下文层与边界改为 `_ROUTE_DOMAIN_CONTEXT` 按 route 查表,7 块正则整体删除。出生时间不确定边界改从矫正闸门的真实状态派生(`effective_accuracy` 不在精确档位,或 `lagna_boundary.is_sensitive`),不再等用户把「出生时间不准」说出来;档位缺失按不确定处理——不知道精度不等于已确认精度。`timing_layers_ready` 改为直接读 `sections``status`,与路由要不要求该层无关。唯一保留文本探测的是 `precise_timing_requested`:「用户有没有要一个具体时间」是服务端没有权威来源的信号,因此改成 timing/annual 路由结构性携带、文本探测仅作叠加,一次措辞漏判不再能把信号清零。
- 验证:`tests/test_consultation_consumer_context.py` 新增 8 条并全绿(该文件共 16 条通过)。8 条覆盖:遍历 `_ROUTE_DEFINITIONS` 断言每条路由都有自己的证据门条目(这是本该拦住本 bug 的守卫),且表里每个层名都能在真实证据包的 sections 里找到;`marriage` 缺 UL 必须报 `degraded``missingLayers == ['UL']``wealth` 缺 D2 同理;性别边界在不含任何关键词的措辞下仍随路由挂上,且不串入其他领域的边界;出生时间边界在「问题完全没提出生时间但精度为 1hour」时出现、在「精度 minute 且 Lagna 不敏感」时不出现;精度档位缺失按不确定处理;`minute` 档位下 Lagna 敏感仍保留边界;`marriage` 路由在 `narayana_dasha` 缺失时 `can_answer_precise_timing` 必须为 false(此时 `missingLayers` 仍为 `[]`、状态仍为 `ready`,正是旧代码放行的那个组合)。已逐条验证这 8 条在旧代码上会失败:旧表下 `marriage`/`wealth` 的门确为 `['D1','D9','dasha_boundaries']`(UL、D2 均不在内),旧正则对该措辞返回 False,旧 `timing_layers_ready``narayana_dasha` 缺失时仍为 True。未做的验证:**没有在 staging 上真实跑一次婚姻类咨询复看回执**,因此「回执与回答不再矛盾」只有单测证据,线上措辞与耗时未观测。
- 待跟进:其一,`birth_time_rectifier.get_effective_accuracy()` 会返回 `'5min'`(声明 minute + 家人清楚记得),而 `ACCURACY_MATRIX` 没有 `'5min'` 这一行,`get_enabled_vargas()` 于是落到 `unknown` 档——比声明 `15min` 更差。本轮只让 `'5min'` 在边界判定上按不确定处理(方向正确),没有补这一行矩阵,分盘可用性仍被低估。其二,`jyotish_api_server.py` 约 7293 行处(穆胡尔塔领域选择)仍有一处同形态的关键词匹配,本轮未动。其三,`projectEvidenceContract()` 的投影范围仍不含 `deterministic_claims_forbidden_for`(见 BUG-256 待跟进),本轮未改投影层。
- 防复发:查表取合同时不得用 `.get(key, 默认值)` 静默兜底——本次三处缺陷里有两处都是「取不到就用一个更宽松的值」,而更宽松的失败方向不会有任何人报错。凡是按路由/领域分派的表,必须有一条测试遍历权威路由集合断言逐条覆盖,并断言表里引用的层名在运行时真实存在;键名与权威定义分处两个文件时,这条测试是唯一能发现键名漂移的机制。已经由模型声明的语义(领域、路由)不得在服务端用文本正则重新推断一遍:重复推断不会更准,只会多出一处静默失效点。判断「某层是否就绪」必须直接读该层的状态,不得借道任何按条件裁剪过的列表——`missing_route_layers` 这类列表为空既可能是齐备也可能是没检查,两者不可区分。
- 相关记录:BUG-259(同一函数上游的路由分歧,本条是「路由定了以后合同没跟上」)、BUG-256(同为回答契约投影/合并层的缺口,其待跟进项与本条同源)、BUG-268(本条的可观测性对照:回执里同样查不到模型有没有读方法)
- 复发自:无
- 修复版本:本地未提交候选
## BUG-268 | 模型有没有真的翻开方法文档,运行结束后无处可查:计数器数完就丢
- 状态:resolved(本地修复,待提交与发布)
- 首次发现:2026-08-18
- 最近更新:2026-08-18
- 影响面:`/api/consult` 的公开回执 `AgentExecutionReceipt` 与服务端可观测事件;影响「web 输出为何不如本地 Agent」这一类问题的可诊断性。
- 用户现象:用户对比本地 Agent 与 web 的输出质量,web 明显更弱。但两份成功回执里能看到的只有 `skill.loaded: true``steps: [skill, tool]``stepBudget.used: 2`,无法回答「模型到底有没有读过方法文档」——而这正是两条链路最可能的差异所在。
- 触发条件:任何一次咨询运行结束后试图回溯模型的方法使用情况。
- 根因:Mastra 的 `skill` 工具返回 SKILL.md 的全文指令加上 references/scripts/assets 的**文件名清单**,真正打开某份参考文档要另调 `skill_read``createConsultationRuntimeHooks` 确实在 `afterToolCall` 里数了 `skillReferenceReadCount`,但这个计数器既没进公开回执,也没进 `agentObservabilityEventSchema`,数完即丢。同时 `skill_read` 按设计不记成 runtime step(只有 `skill`/`tool`/`validation` 三类会记),所以 `steps``stepBudget.used` 天然看不见它;服务端日志里唯一能间接反映的是 `modelStepCount`。结果是:唯一能直接回答该问题的数字被算出来后丢弃,只留下一个需要推断的替代量。
- 修复:把 `referenceReads` 加进回执的 `skill` 对象,并设为**必填**而非可选——正是「可选且没人填」让这个数字消失的,必填能让将来任何一处新的回执构造点无法再省掉它。同时把 `skillReferenceReads` 加进 `consultationModelStepTelemetry()`,它已被展开进可观测日志,因此服务端日志自动与 `modelStepCount` 并列拿到该值。两个构造点(工具可选路径与强制工具路径)都改为从 `state.skillReferenceReadCount` 读取。
- 验证:`frontend/tests/consultation-agentic-runtime.test.ts` 37 项通过,其中新增 1 项:走真实 hooks,断言加载 skill 后计数仍为 0(说明「已加载」不等于「读过方法」)、两次成功的参考读取记为 2、失败的读取不计数、参考读取不出现在 steps 里(因此 steps 无法替代该字段),最后断言回执解析后 `skill.referenceReads === 2`。既有断言同步收紧:`consultationModelStepTelemetry()` 的期望值现在包含 `skillReferenceReads``tsc --noEmit` 清洁。未做的验证:**没有在 staging 上取一次真实回执**,因此线上那两次运行的 `referenceReads` 究竟是 0 还是别的值仍未观测——这正是本条要让它可观测的那个数。
- 待跟进:拿到线上真实值后再判断下一步。若确认为 0,则「web 不如本地」的主因是方法文档在步数预算内从未被打开,方向应是让方法可达(预算、指令、或把关键方法上提进 SKILL.md),而不是继续加服务端约束。
- 防复发:被数出来的诊断量必须有一个出口(回执或可观测日志),否则等于没数。当某个字段的作用正是「证明某件事发生过或没发生过」时,它在 schema 里应当必填:可选字段缺失与「值为 0」在下游无法区分,而这里 0 恰恰是最需要被看见的答案。另外,不要用 `steps` / `stepBudget.used` 推断模型的全部动作——这两者只记录被显式登记的三类步骤,模型的其余工具调用在其中不可见。
- 相关记录:BUG-267(同一批手测暴露的另一处静默缺口)、BUG-255(步数预算被浪费,当时也依赖 `modelStepCount` 这一间接量定位)、BUG-258(同为「失败/结束时回执信息不足」的形态)
- 复发自:无
- 修复版本:本地未提交候选