{ "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 本期降级说明 + ⏸ 本期降级" }, { "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,否则前端问答直接连不上" } }