Files
group_fqcd_jr/docs/evidence/20260919-t2k-c10-source-reference-downgrade.json
T

166 lines
8.5 KiB
JSON
Raw Normal View History

{
"artifact": "t2k-c10-source-reference-downgrade",
"generated_at": "2026-09-19T01:41:43.772361+00:00",
"date_local": "2026-09-19",
"purpose": "执行 C-10「知识出口来源引用」(关键路径 C-01 → C-03 → C-09 → C-06 → C-07 → C-10 的收口步);裁定取「乙 · 本期降级」并加双向护栏",
"scope": [
"app/service/agent/implementations/customer_service.py(业务层:_references 降级为死代码并写明依据)",
"tests/unit/service/test_customer_service_agent.py(护栏 A:AST 守无调用点)",
"tests/unit/service/test_agent_governance.py(护栏 B:knowledge 来源必被拒)",
"开发文档对应的 D2.2 / D2.4(口径回写,另见 D2.1 v6.6 / D1.6 §4.19)"
],
"decision": {
"chosen": "乙 · 本期降级(不向客户展示来源引用)",
"rejected": "甲 · 底座方先登记知识来源并放行(需会签)",
"why_b": "甲依赖底座方改造 ToolExecutor 与治理层放行 knowledge 来源,属不可控外部依赖;本模块今天要交付,不能把交付卡在别人会签上。乙的代价只是「不展示出处」,可追溯性并未丢失(见下)。",
"why_not_silent": "降级不是静默丢弃:D2.2 的 FR-CS-010 与验收表、D2.4 的 §5.7 与 G4/AC-06 都写明了「本期降级」及其技术原因与启用前提,避免下一轮误当成已完成功能。",
"reversibility": "完全可逆:代码本体(_references)保留在位,护栏只是禁止调用;底座方放行后删掉护栏断言即可接通。"
},
"technical_reason": {
"gate": "app/service/agent/governance.py:314-316(review_output 的来源白名单只认 memory / tool)",
"failure_mode": "knowledge 来源会被判为「引用未来自本次已授权召回结果」,抛出 ForbiddenAgentError ⇒ **整个 run 失败**,不是降级、也不是少一个字段",
"red_line": "S-8",
"traceability_instead": "可追溯性由审计承接:agent.tool_executed 的工具调用记录含命中 doc_id 与分数;消息表亦留痕"
},
"implementation": {
"docstring": "_references() 的 docstring 重写为「保留待用的死代码 —— 本期(MVP)不向客户展示来源引用」,写明 governance.py:314-316、S-8、审计承接、启用前提须会签",
"helper_state": "函数体原样保留(source_type=\"knowledge\" 的 SourceReference 组装逻辑未改)",
"no_call_sites": "改用其他出口返回正文;零调用点"
},
"guardrails": {
"A_ast_no_call": {
"file": "tests/unit/service/test_customer_service_agent.py",
"test": "test_knowledge_exit_never_calls_the_disabled_reference_helper",
"method": "ast.walk 找 ast.Call 且 func.attr == \"_references\",断言 calls == []",
"why_ast": "该名字会合法出现在注释与文档字符串里,纯文本搜索会把「提一句」误判成「调用」",
"second_assert": "同时断言 _references 函数定义仍在位(清理时不得被连带删除)"
},
"B_governance_rejects": {
"file": "tests/unit/service/test_agent_governance.py",
"test": "test_knowledge_reference_is_rejected_so_it_must_stay_disabled",
"method": "以 source_type=\"knowledge\" 的 SourceReference 调 review_output,断言抛 ForbiddenAgentError",
"why": "把「本期为何必须降级」的技术原因钉成可执行断言:若将来底座放行,这个测试会失败并提醒同步移除护栏 A"
}
},
"html_downgrade_points": [
{
"doc": "D2.2",
"locator": "FR-CS-010 行",
"change": "追加 ⏸ 2026-09-19 本期降级说明 + <span class=\"pill p2\">⏸ 本期降级</span>"
},
{
"doc": "D2.2",
"locator": "US-CS-01 行",
"change": "期望值补「⏸ 本期不展示来源引用:C-10 乙」"
},
{
"doc": "D2.2",
"locator": "验收表 · 一 · 功能正确性",
"change": "「每条须带来源引用」→「⏸ 本期豁免(改由审计承接)」"
},
{
"doc": "D2.4",
"locator": "§5.7 末尾",
"change": "追加降级段:governance.py:314-316、S-8、_references() 死代码 + 护栏、启用前提须会签"
},
{
"doc": "D2.4",
"locator": "G4 行",
"change": "「可追溯」补「⏸ 本期:来源引用降级,可追溯性由审计承接」"
},
{
"doc": "D2.4",
"locator": "§5.7 三条要求 ①",
"change": "「知识类回答必须附来源」补「⏸ 本期降级,见下段」(本轮新发现的口径残留,与降级冲突)"
},
{
"doc": "D2.4",
"locator": "AC-06 行",
"change": "判据补「⏸ 本期豁免:C-10 乙不展示来源引用,改由审计承接」(同上,本轮新发现)"
}
],
"eol_check": {
"D2.2": "CRLF(本轮编辑后复核)",
"D2.4": "LF —— 编辑后实测 bytes=147917, bareLF=1758, CRLF=0,与改前同为纯 LF,EOL 未被改写"
},
"gates": {
"targeted_pytest": {
"files": 4,
"passed": 164,
"failed": 0,
"command": "pytest -q tests/unit/service/test_customer_service_agent.py tests/unit/service/test_agent_governance.py tests/unit/core/test_customer_service_rules.py tests/unit/core/test_compliance_context.py"
},
"ruff": {
"scope": "app tests tools",
"errors": 22,
"note": "等于基线,无新增"
},
"mypy": {
"scope": "app",
"errors": 3,
"note": "等于基线(agent_persistence_service.py:82 / run_query_service.py:79 / promotion_renderer.py:84,均为既有问题)"
}
},
"not_done_in_this_step": [],
"open_questions": {
"C-10_enablement": "若后续要展示出处,需底座方:① ToolExecutor 把工具返回的知识 doc_id 登记为本次可引用来源;② 治理层放行 knowledge 来源。两步都要会签。"
},
"closure_after": {
"document_writeback": "已完成:D2.1 升 v6.6(C-10 行标 [x] + 修订要点)、D1.6 新增 §4.19(第十五轮会话记录)",
"full_gate": "已完成,见 full_gate"
},
"full_gate": {
"pytest": {
"command": "pytest -q -p no:cacheprovider",
"result": "2 failed / 1579 passed / 2 skipped",
"note": "2 failed = T0 基线同两项(profile_snapshot_current_invariant / worker_runtime[False]),不计入本轮回归;passed 1577 → 1579,增量 = 本步新增的 2 条护栏"
},
"ruff": {
"command": "ruff check app tests tools",
"errors": 22,
"note": "= 基线"
},
"mypy": {
"command": "mypy app",
"errors": 3,
"note": "= 基线"
},
"authoritative_docs": {
"command": "python tools/check_authoritative_docs.py",
"result": "checked 50 documents, no number collision"
}
},
"environment_finding": {
"title": "残留 Worker 抢消费 outbox 事件 ⇒ 集成测试偶发失败(环境级,非代码回归)",
"first_symptom": "全量首跑多出第 3 个失败:tests/integration/test_memory_extraction.py::test_memory_is_not_recallable_across_customers",
"diagnosis_steps": [
"① 单文件重跑两次 → 3/3 通过 ⇒ 不稳定,不是确定性缺陷",
"② 按同一链路脚本复现 seed → enqueue → consume_once → 返回 True ⇒ 消费机制本身正常",
"③ 无人消费检测:插入一条 pending 事件后只观察、不消费 → 1 秒内被外部进程改成 published ⇒ 存在外部消费者"
],
"root_cause": "2026-09-18 17:52 启动的残留服务一直存活,其中 python -m app.worker 会抢消费 outbox 事件,与测试自身的 consume_once(aggregate_id=...) 竞争,先到先得 ⇒ 测试偶发拿不到事件而返回 False",
"processes": [
{
"pid": 27204,
"child": 8740,
"cmd": "uvicorn app.main:app --host 127.0.0.1 --port 8000"
},
{
"pid": 27084,
"child": 2204,
"cmd": "python -m app.worker"
}
],
"action": "经用户批准,已 Stop-Process 停掉 4 个 PID;停止后全量 pytest 回到 2 failed 基线",
"reusable_lessons": [
"与 A-02「门禁基线前先停 Worker」完全吻合 —— 该前置不是形式主义,不停就会污染集成测试",
"印证 S-7:知识集合重建后这两个服务早已过期",
"遇到集成测试失败,先做「无人消费检测」排除外部消费者,再怀疑代码"
]
},
"demo_precondition": {
"issue": "API 与 Worker 现已停止(8000 端口无监听)",
"must_do": "演示前按 S-7 重启 API + Worker,否则前端问答直接连不上"
}
}