release: v0.20 Codex-ready skill-driven core

This commit is contained in:
kai
2026-05-07 08:21:28 +08:00
parent 0644a68ecc
commit 68e45bcf41
45 changed files with 3005 additions and 157 deletions
+53
View File
@@ -62,6 +62,10 @@ def write_packet(path: Path, task_id: str, claim: str, source_id: str) -> None:
"evidence_items": [{"source_id": source_id, "summary": f"{claim} 的证据"}],
"counter_evidence": [{"claim": "仍需关注样本量和外推限制", "source_ids": ["src_counter"]}],
"source_ids": [source_id, "src_counter"],
"sources": [
{"id": source_id, "title": "来源", "url": f"https://example.com/{source_id}"},
{"id": "src_counter", "title": "反方来源", "url": "https://example.com/counter"},
],
"source_quality_notes": [f"{source_id} Tier 1"],
"open_questions": ["还需要补充中国市场数据"],
"raw_quotes_or_notes": ["English note can remain as source material."],
@@ -76,6 +80,7 @@ def test_build_chapter_briefs_aggregates_packets_by_chapter(tmp_path: Path) -> N
{
"task_id": "ch01-clinical",
"chapter_ids": ["ch01"],
"chapter_title": "临床证据正在重塑需求判断",
"topic_axis": "clinical",
"questions": ["q"],
"search_routes": ["scholar"],
@@ -84,6 +89,7 @@ def test_build_chapter_briefs_aggregates_packets_by_chapter(tmp_path: Path) -> N
{
"task_id": "ch01-market",
"chapter_ids": ["ch01"],
"chapter_title": "临床证据正在重塑需求判断",
"topic_axis": "market",
"questions": ["q"],
"search_routes": ["news"],
@@ -101,6 +107,7 @@ def test_build_chapter_briefs_aggregates_packets_by_chapter(tmp_path: Path) -> N
brief = briefs[0]
validate_chapter_brief(brief)
assert brief["chapter_id"] == "ch01"
assert brief["chapter_title"] == "临床证据正在重塑需求判断"
assert brief["packet_ids"] == ["ch01-clinical", "ch01-market"]
assert "src_001" in brief["source_ids"]
assert "src_002" in brief["source_ids"]
@@ -118,6 +125,50 @@ def test_build_chapter_briefs_aggregates_packets_by_chapter(tmp_path: Path) -> N
assert (project / "phase2/compressed_findings/ch01.json").exists()
def test_build_chapter_briefs_skips_placeholder_packets(tmp_path: Path) -> None:
project = tmp_path / "project"
cards = [
{
"task_id": "ch01-good",
"chapter_ids": ["ch01"],
"chapter_title": "临床证据正在重塑需求判断",
"topic_axis": "clinical",
"questions": ["q"],
"search_routes": ["scholar"],
"output_packet": "phase2/packets/ch01-good.json",
},
{
"task_id": "ch01-empty",
"chapter_ids": ["ch01"],
"chapter_title": "临床证据正在重塑需求判断",
"topic_axis": "clinical",
"questions": ["q"],
"search_routes": ["scholar"],
"output_packet": "phase2/packets/ch01-empty.json",
},
]
(project / "phase2").mkdir(parents=True)
(project / "phase2/task_cards.json").write_text(json.dumps(cards, ensure_ascii=False), encoding="utf-8")
write_packet(project / "phase2/packets/ch01-good.json", "ch01-good", "临床证据支持核心判断", "src_001")
empty = {
"task_id": "ch01-empty",
"claims": [],
"evidence_items": [],
"counter_evidence": [],
"source_ids": [],
"source_quality_notes": [],
"open_questions": [],
"raw_quotes_or_notes": [],
}
(project / "phase2/packets/ch01-empty.json").write_text(json.dumps(empty, ensure_ascii=False), encoding="utf-8")
briefs = build_chapter_briefs(project)
assert len(briefs) == 1
warnings = json.loads((project / "phase2/brief_warnings.json").read_text(encoding="utf-8"))
assert warnings[0]["task_id"] == "ch01-empty"
def test_chapter_prompt_contains_brief_and_fragmentation_guard() -> None:
brief = {
"chapter_id": "ch01",
@@ -161,6 +212,8 @@ def test_chapter_assembly_worker_writes_markdown(tmp_path: Path) -> None:
assert output == tmp_path / "phase2/drafts/ch01.md"
assert "结论先行" in output.read_text(encoding="utf-8")
assert fake.calls[0]["model"] == role.model
assert "章节证据分析师" in fake.calls[0]["system"]
assert "中文章节组装 worker" in fake.calls[0]["system"]
def test_validate_chapter_markdown_rejects_unknown_source_ids() -> None:
+10 -1
View File
@@ -68,13 +68,22 @@ def test_framework_mentions_ingested_materials(tmp_path: Path) -> None:
assert "phase0/extracted/audit.md" in framework
assert "NMPA、FDA、EMA、ICH、WHO" in framework
assert "Phase1 的职责是大胆假设" in framework
assert "本章要解决的问题" in framework
assert "请先确认 `phase1/material_brief.md`" in framework
assert research_brief_md.exists()
assert "任务切分原则" in research_brief_md.read_text(encoding="utf-8")
assert "章节命题与求证计划" in research_brief_md.read_text(encoding="utf-8")
assert (project / "phase1" / "hypothesis_map.json").exists()
assert brief["research_method"] == "gmp_quality_operations_diagnosis"
assert brief["work_language"] == "zh"
assert brief["phase2_mode"] == "chapter_integrated"
assert brief["central_thesis"]
assert brief["chapter_planning"][0]["phase2_prompt_context"]
assert brief["task_planning"]["required_skills"]
assert brief["task_planning"]["search_routes_by_axis"]["counter"] == ["scholar", "general"]
assert brief["task_planning"]["phase2_mode"] == "chapter_integrated"
assert brief["task_planning"]["search_routes_by_axis"]["counter"] == ["fda", "scholar", "evidence", "general"]
assert brief["task_planning"]["search_routes_by_axis"]["quality_system_gap"] == ["fda", "evidence", "general"]
assert brief["phase2_inputs"]["framework_path"] == "phase1/framework.md"
+80
View File
@@ -0,0 +1,80 @@
from __future__ import annotations
import json
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
from scripts.runtime.review import (
build_phase3_model_review_context,
build_phase3_model_critique,
phase3_model_review_system_prompt,
)
class FakeClient:
def __init__(self) -> None:
self.calls: list[dict[str, object]] = []
def chat_complete(self, **kwargs) -> str:
self.calls.append(kwargs)
return "# Phase 3 Opus 4.7 独立审校\n\n## 总体判定\n\n回炉 Phase2。\n"
def make_project(tmp_path: Path) -> Path:
project = tmp_path / "project"
(project / "phase1").mkdir(parents=True)
(project / "phase2/drafts").mkdir(parents=True)
(project / "phase2/compressed_findings").mkdir(parents=True)
(project / "manifest.json").write_text(
json.dumps({"topic": "白帆测试项目", "phase3": {}}, ensure_ascii=False),
encoding="utf-8",
)
(project / "phase1/framework.md").write_text("## 第1章 质量体系判断\n", encoding="utf-8")
(project / "phase2/drafts/ch01.md").write_text("## 质量体系判断\n\n正文。[src_001]\n", encoding="utf-8")
(project / "phase2/sources.jsonl").write_text('{"id":"src_001","title":"来源","url":"https://www.fda.gov/example"}\n', encoding="utf-8")
(project / "phase2/compressed_findings/ch01.json").write_text(
json.dumps(
{
"chapter_id": "ch01",
"chapter_title": "质量体系判断",
"packet_ids": ["ch01-a"],
"chapter_thesis": "质量体系需要补证据",
"key_findings": [],
"evidence_landings": [],
"counter_evidence": [],
"source_ids": ["src_001"],
"open_questions": [],
"writing_plan": [],
},
ensure_ascii=False,
),
encoding="utf-8",
)
return project
def test_phase3_model_context_contains_structured_inputs(tmp_path: Path) -> None:
project = make_project(tmp_path)
context = build_phase3_model_review_context(project)
assert "Deterministic Review Baseline" in context
assert "Compressed Findings" in context
assert "Chapter Drafts" in context
assert "src_001" in context
def test_phase3_model_review_calls_requested_model_and_writes_critique(tmp_path: Path) -> None:
project = make_project(tmp_path)
fake = FakeClient()
out = build_phase3_model_critique(project, client=fake, model="zenmux-anthropic/claude-opus-4-7")
assert out.exists()
assert fake.calls[0]["model"] == "zenmux-anthropic/claude-opus-4-7"
assert "独立总编审校" in fake.calls[0]["system"]
assert "FDA/NMPA/EMA/ICH/WHO" in phase3_model_review_system_prompt()
+19
View File
@@ -0,0 +1,19 @@
from __future__ import annotations
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
from scripts.polish import build_polish_system_prompt
def test_polish_system_prompt_loads_humanizer_and_output_hygiene() -> None:
prompt = build_polish_system_prompt()
assert "# Skill: humanizer-cn" in prompt
assert "CN-1" in prompt
assert "# Skill: output-hygiene" in prompt
assert "禁止词" in prompt
+36
View File
@@ -10,6 +10,7 @@ if str(REPO_ROOT) not in sys.path:
from scripts.reporting.fonts import resolve_quarto_fonts
from scripts.reporting.references import build_references_block
from scripts.number_citations import number_citations
def test_build_references_block_uses_only_cited_sources(tmp_path: Path) -> None:
@@ -32,3 +33,38 @@ def test_resolve_quarto_fonts_returns_stable_defaults_for_missing_dir(tmp_path:
assert fonts.main_font == "Source Han Serif CN"
assert fonts.sans_font == "Source Han Sans CN"
assert fonts.requires_system_fonts is True
def test_number_citations_replaces_source_ids_and_keeps_url() -> None:
text = "# 报告\n\n关键判断。[src_a, src_b]\n\n## 参考文献\n\n旧列表\n"
sources = {
"src_a": {"title": "法规 A", "url": "https://example.com/a"},
"src_b": {"title": "指南 B", "url": "https://example.com/b"},
}
numbered, records = number_citations(text=text, sources=sources)
assert "关键判断。<sup>[1, 2]</sup>" in numbered
assert "[src_a" not in numbered
assert "## 参考来源清单" in numbered
assert "1. 法规 A. https://example.com/a" in numbered
assert "旧列表" not in numbered
assert [record["source_id"] for record in records] == ["src_a", "src_b"]
def test_number_citations_deduplicates_same_underlying_source() -> None:
text = "甲。[src_a]\n\n乙。[src_b, src_c]\n"
sources = {
"src_a": {"title": "同一报告 OCR", "path": "phase0/report.md"},
"src_b": {"title": "同一报告", "path": "phase0/report.md"},
"src_c": {"title": "法规 C", "url": "https://example.com/c"},
}
numbered, records = number_citations(text=text, sources=sources)
assert "甲。<sup>[1]</sup>" in numbered
assert "乙。<sup>[1, 2]</sup>" in numbered
assert numbered.count("同一报告") == 1
assert "同一报告 OCR" not in numbered
assert len(records) == 2
assert records[0]["source_ids"] == ["src_a", "src_b"]
+159 -8
View File
@@ -11,7 +11,7 @@ if str(REPO_ROOT) not in sys.path:
from scripts.runtime.roles import resolve_runtime_profile
from scripts.runtime.sources import append_packet_sources, rebuild_sources_from_packets
from scripts.runtime.tasks import TaskCard
from scripts.runtime.workers import PacketWorker, build_search_context
from scripts.runtime.workers import PacketWorker, build_material_context, build_route_query, build_search_context, normalize_packet_against_context
class FakeSearchProvider:
@@ -57,6 +57,66 @@ def test_build_search_context_assigns_stable_source_ids() -> None:
assert context["routes_used"] == ["scholar", "general"]
def test_fda_route_query_uses_english_axis_terms_not_chinese_title() -> None:
card = TaskCard(
task_id="ch10-fda_enforcement_precedents",
chapter_ids=["ch10"],
topic_axis="fda_enforcement_precedents",
questions=["立即纠偏、体系补强、能力建设三层整改路线图必须绑定 owner、关闭证据和复核机制"],
search_routes=["fda"],
output_packet="phase2/packets/ch10-fda_enforcement_precedents.json",
chapter_title="立即纠偏、体系补强、能力建设三层整改路线图必须绑定 owner、关闭证据和复核机制",
)
query = build_route_query(card, "fda")
assert "立即纠偏" not in query
assert "CAPA" in query
assert "remediation" in query
assert "verification evidence" in query
def test_integrated_scholar_query_does_not_leak_internal_axis_or_cjk_punctuation() -> None:
card = TaskCard(
task_id="ch07-chapter_integrated",
chapter_ids=["ch07"],
topic_axis="chapter_integrated",
questions=["人员能力:培训有效性比培训记录更关键"],
search_routes=["scholar"],
output_packet="phase2/packets/ch07-chapter_integrated.json",
chapter_title="人员能力:培训有效性比培训记录更关键",
)
query = build_route_query(card, "scholar")
assert "chapter_integrated" not in query
assert "" not in query
assert " " not in query
assert "training" in query
assert "quality" in query
assert not any("\u4e00" <= char <= "\u9fff" for char in query)
def test_evidence_route_query_is_short_english_candidate_evidence_query() -> None:
card = TaskCard(
task_id="ch08-chapter_integrated",
chapter_ids=["ch08"],
topic_axis="chapter_integrated",
questions=["运营管理需要建立跨部门节奏、问题升级、指标看板和管理层 review"],
search_routes=["evidence"],
output_packet="phase2/packets/ch08-chapter_integrated.json",
chapter_title="运营管理需要建立跨部门节奏、问题升级、指标看板和管理层 review",
)
query = build_route_query(card, "evidence")
assert "evidence" in query
assert "quality" in query
assert "operations" in query
assert "运营管理" not in query
assert not any("\u4e00" <= char <= "\u9fff" for char in query)
def test_packet_worker_includes_search_context_in_prompt() -> None:
context = build_search_context(sample_card(), FakeSearchProvider(), num_results_per_route=1)
response = {
@@ -79,7 +139,71 @@ def test_packet_worker_includes_search_context_in_prompt() -> None:
assert "candidate_sources" in fake.calls[0]["user"]
def test_append_packet_sources_dedupes_by_url(tmp_path: Path) -> None:
def test_normalize_packet_fills_source_ids_and_sources_from_context() -> None:
context = {
"candidate_sources": [
{"id": "src_a", "title": "A", "url": "https://example.com/a", "tier": "Tier 2", "score": 7}
]
}
packet = {
"task_id": "ch01",
"claims": [{"claim": "判断", "source_ids": ["src_a"]}],
"evidence_items": [{"source_id": "src_a", "summary": "证据"}],
"counter_evidence": [{"claim": "反方", "source_ids": ["src_a"]}],
"source_quality_notes": [],
"open_questions": [],
"raw_quotes_or_notes": [],
}
normalized = normalize_packet_against_context(packet, context, None)
assert normalized["source_ids"] == ["src_a"]
assert normalized["sources"] == context["candidate_sources"]
def test_material_context_is_loaded_and_allowed_as_source(tmp_path: Path) -> None:
project = tmp_path / "project"
material = project / "phase0/extracted/audit.md"
material.parent.mkdir(parents=True)
material.write_text("白帆现场发现:偏差调查未闭环。", encoding="utf-8")
card = TaskCard(
task_id="ch01-chapter_integrated",
chapter_ids=["ch01"],
topic_axis="chapter_integrated",
questions=["q"],
search_routes=[],
output_packet="phase2/packets/ch01-chapter_integrated.json",
allowed_materials=["phase0/extracted/audit.md"],
)
context = build_material_context(card, project)
response = {
"task_id": "ch01-chapter_integrated",
"claims": [{"claim": "现场材料显示偏差调查需要补强", "source_ids": [context["materials"][0]["source_id"]]}],
"evidence_items": [{"source_id": context["materials"][0]["source_id"], "summary": "偏差调查未闭环。"}],
"counter_evidence": [{"claim": "需与完整审计报告交叉确认", "source_ids": [context["materials"][0]["source_id"]]}],
"source_ids": [context["materials"][0]["source_id"]],
"sources": [
{
"id": context["materials"][0]["source_id"],
"title": "audit.md",
"url": "phase0/extracted/audit.md",
"tier": "local_material",
}
],
"source_quality_notes": ["本地材料作为起点证据"],
"open_questions": [],
"raw_quotes_or_notes": ["白帆现场发现:偏差调查未闭环。"],
}
fake = FakeClient(response)
role = resolve_runtime_profile(profile="medium").role_for_task("evidence_packet")
packet = PacketWorker(role=role, client=fake, project_root=project).run(card)
assert packet["source_ids"] == [context["materials"][0]["source_id"]]
assert "白帆现场发现" in fake.calls[0]["user"]
def test_append_packet_sources_preserves_distinct_source_ids_for_same_url(tmp_path: Path) -> None:
packet = {
"sources": [
{"id": "src_a", "title": "A", "url": "https://example.com/a", "tier": "Tier 2", "score": 7},
@@ -89,11 +213,11 @@ def test_append_packet_sources_dedupes_by_url(tmp_path: Path) -> None:
written = append_packet_sources(tmp_path / "sources.jsonl", packet)
assert written == 1
assert len((tmp_path / "sources.jsonl").read_text(encoding="utf-8").splitlines()) == 1
assert written == 2
assert len((tmp_path / "sources.jsonl").read_text(encoding="utf-8").splitlines()) == 2
def test_rebuild_sources_from_packets_dedupes_manual_packets(tmp_path: Path) -> None:
def test_rebuild_sources_from_packets_preserves_distinct_source_ids(tmp_path: Path) -> None:
project = tmp_path / "project"
packets = project / "phase2" / "packets"
packets.mkdir(parents=True)
@@ -109,7 +233,34 @@ def test_rebuild_sources_from_packets_dedupes_manual_packets(tmp_path: Path) ->
count = rebuild_sources_from_packets(project)
lines = (project / "phase2" / "sources.jsonl").read_text(encoding="utf-8").splitlines()
assert count == 2
assert len(lines) == 2
assert count == 3
assert len(lines) == 3
assert "src_001" in lines[0]
assert "src_003" in lines[1]
assert "src_002" in lines[1]
assert "src_003" in lines[2]
def test_rebuild_sources_preserves_cache_metadata(tmp_path: Path) -> None:
project = tmp_path / "project"
packets = project / "phase2" / "packets"
packets.mkdir(parents=True)
source = {"id": "src_001", "title": "A", "url": "https://example.com/a"}
(packets / "ch01-a.json").write_text(json.dumps({"sources": [source]}, ensure_ascii=False), encoding="utf-8")
(project / "phase2/sources.jsonl").write_text(
json.dumps(
{
**source,
"cached_text_path": "phase2/source_cache/md/src_001.md",
"cache_status": "fetched",
},
ensure_ascii=False,
)
+ "\n",
encoding="utf-8",
)
rebuild_sources_from_packets(project)
row = json.loads((project / "phase2/sources.jsonl").read_text(encoding="utf-8"))
assert row["cached_text_path"] == "phase2/source_cache/md/src_001.md"
assert row["cache_status"] == "fetched"
+70
View File
@@ -0,0 +1,70 @@
from __future__ import annotations
import json
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
from scripts.runtime.source_cache import cache_sources, is_important_source
class FakeResponse:
headers = {"content-type": "text/html; charset=utf-8"}
url = "https://www.fda.gov/example"
content = b"<html><body><h1>FDA Guidance</h1><p>Important CGMP text.</p></body></html>"
def raise_for_status(self) -> None:
return None
class FakeClient:
def __enter__(self) -> "FakeClient":
return self
def __exit__(self, *_args) -> None:
return None
def get(self, url: str) -> FakeResponse:
assert url == "https://www.fda.gov/example"
return FakeResponse()
def close(self) -> None:
return None
def test_is_important_source_detects_official_regulator() -> None:
assert is_important_source({"url": "https://www.fda.gov/example", "title": "FDA"})
assert not is_important_source({"url": "https://example.com/blog", "title": "Blog"})
def test_cache_sources_writes_markdown_and_updates_registry(tmp_path: Path, monkeypatch) -> None:
project = tmp_path / "project"
sources = project / "phase2" / "sources.jsonl"
sources.parent.mkdir(parents=True)
sources.write_text(
json.dumps(
{
"id": "src_fda_001",
"title": "FDA Guidance",
"url": "https://www.fda.gov/example",
"tier": "Tier 1",
},
ensure_ascii=False,
)
+ "\n",
encoding="utf-8",
)
monkeypatch.setattr("scripts.runtime.source_cache.httpx.Client", lambda **_kwargs: FakeClient())
results = cache_sources(project)
rows = [json.loads(line) for line in sources.read_text(encoding="utf-8").splitlines()]
assert len(results) == 1
assert rows[0]["cached_text_path"].startswith("phase2/source_cache/md/")
cached = project / rows[0]["cached_text_path"]
assert cached.exists()
assert "Important CGMP text." in cached.read_text(encoding="utf-8")
+98
View File
@@ -68,6 +68,10 @@ def test_research_build_briefs_does_not_overwrite_existing_packets(tmp_path: Pat
"evidence_items": [{"source_id": "src_001", "summary": "证据"}],
"counter_evidence": [{"claim": "限制", "source_ids": ["src_002"]}],
"source_ids": ["src_001", "src_002"],
"sources": [
{"id": "src_001", "title": "来源1", "url": "https://example.com/1"},
{"id": "src_002", "title": "来源2", "url": "https://example.com/2"},
],
"source_quality_notes": ["src_001 Tier 1"],
"open_questions": [],
"raw_quotes_or_notes": [],
@@ -80,6 +84,28 @@ def test_research_build_briefs_does_not_overwrite_existing_packets(tmp_path: Pat
assert "真实证据不能被 skeleton 覆盖" in packet_path.read_text(encoding="utf-8")
def test_codex_native_profile_does_not_claim_python_core_model_execution(tmp_path: Path) -> None:
project = tmp_path / "project"
(project / "phase1").mkdir(parents=True)
(project / "manifest.json").write_text(
'{"research_method": "mckinsey_market", "phase1": {"approved": true}, "phase2": {}}\n',
encoding="utf-8",
)
(project / "phase1/framework.md").write_text(
"## 第1章 临床证据正在重塑需求判断\n\n研究思路。",
encoding="utf-8",
)
args = dr.build_parser().parse_args(["research", str(project), "--profile", "codex_native", "--execute-packets"])
try:
dr.cmd_research(args)
except SystemExit as exc:
assert "not Codex App built-in models" in str(exc)
else:
raise AssertionError("codex_native must not execute through Python external clients")
def test_packet_state_counts_ignores_stale_errors_for_ready_packets(tmp_path: Path) -> None:
project = tmp_path / "project"
(project / "phase2/packets").mkdir(parents=True)
@@ -184,6 +210,30 @@ def test_init_and_frame_create_executable_python_core_project(tmp_path: Path) ->
assert "中文" in framework
def test_frame_can_preserve_existing_outline(tmp_path: Path) -> None:
project = tmp_path / "custom-outline"
(project / "phase1").mkdir(parents=True)
(project / "manifest.json").write_text(
'{"topic": "自定义研究", "research_method": "mckinsey_market", "target_words": 12000, "phase1": {}}\n',
encoding="utf-8",
)
(project / "phase1/framework.md").write_text(
"## 第1章 第一条自定义主线\n\n## 第2章 第二条自定义主线\n\n## 第3章 第三条自定义主线\n\n"
"## 第4章 第四条自定义主线\n\n## 第5章 第五条自定义主线\n\n## 第6章 第六条自定义主线\n\n"
"## 第7章 第七条自定义主线\n\n## 第8章 第八条自定义主线\n",
encoding="utf-8",
)
args = dr.build_parser().parse_args(["frame", str(project), "--preserve-existing-outline"])
assert dr.cmd_frame(args) == 0
framework = (project / "phase1/framework.md").read_text(encoding="utf-8")
brief = json.loads((project / "phase1/research_brief.json").read_text(encoding="utf-8"))
assert "第一条自定义主线" in framework
assert "本章要解决的问题" in framework
assert brief["chapter_planning"][0]["title"] == "第一条自定义主线"
def test_review_writes_phase3_critique(tmp_path: Path) -> None:
project = tmp_path / "project"
(project / "phase1").mkdir(parents=True)
@@ -205,6 +255,54 @@ def test_review_writes_phase3_critique(tmp_path: Path) -> None:
assert "src_001" in text
def test_review_model_dry_run_exposes_opus_context_plan(tmp_path: Path, capsys) -> None:
project = tmp_path / "project"
project.mkdir()
(project / "manifest.json").write_text('{"topic": "测试项目"}\n', encoding="utf-8")
args = dr.build_parser().parse_args(["review", str(project), "--model-review", "--dry-run"])
assert dr.cmd_review(args) == 0
out = capsys.readouterr().out
assert "zenmux-anthropic/claude-opus-4-7" in out
assert "review_context_opus_4_7.md" in out
def test_finalize_polish_dry_run_uses_polish_source_argument(tmp_path: Path, capsys) -> None:
project = tmp_path / "project"
(project / "phase4").mkdir(parents=True)
(project / "manifest.json").write_text(
'{"model_profile": "medium", "phase4": {}}\n',
encoding="utf-8",
)
(project / "phase4/final_zh.md").write_text("# 中文终稿\n", encoding="utf-8")
args = dr.build_parser().parse_args(["finalize", str(project), "--polish", "--dry-run"])
assert dr.cmd_finalize(args) == 0
out = capsys.readouterr().out
assert "scripts/polish.py" in out
assert "--source phase4/final_zh.md" in out
assert "--input phase4/final_zh.md" not in out.split("scripts/polish.py", 1)[1]
def test_finalize_number_citations_dry_run_builds_numbered_markdown(tmp_path: Path, capsys) -> None:
project = tmp_path / "project"
(project / "phase4").mkdir(parents=True)
(project / "manifest.json").write_text(
'{"model_profile": "medium", "phase4": {}}\n',
encoding="utf-8",
)
(project / "phase4/final_zh.md").write_text("# 中文终稿\n\n正文。[src_001]\n", encoding="utf-8")
args = dr.build_parser().parse_args(["finalize", str(project), "--number-citations", "--dry-run"])
assert dr.cmd_finalize(args) == 0
out = capsys.readouterr().out
assert "scripts/number_citations.py" in out
assert "--input phase4/final_zh_numbered.md" in out
def test_run_new_topic_initializes_and_frames_project(tmp_path: Path) -> None:
args = dr.build_parser().parse_args(
[
+123
View File
@@ -12,6 +12,8 @@ if str(REPO_ROOT) not in sys.path:
from scripts.lib.model_config import resolve_model_profile
from scripts.runtime.roles import resolve_runtime_profile
from scripts.runtime.methods import ResearchMethodRegistry
from scripts.runtime.phase1 import build_chapter_planning
from scripts.runtime.skills import SkillRegistry
from scripts.runtime.tasks import (
TaskCard,
@@ -78,6 +80,7 @@ def test_generate_task_cards_from_chinese_framework() -> None:
"ch02-regulatory",
]
assert cards[0].output_packet == "phase2/packets/ch01-clinical.json"
assert cards[0].chapter_title == "GLP-1 产业链的增量来自适应症扩张"
assert cards[0].research_goal
assert "search-gateway" in cards[0].required_skills
assert cards[0].expected_evidence["min_tier_1_2_sources"] == 2
@@ -121,6 +124,122 @@ def test_generate_task_cards_from_research_brief_carries_prompt_and_skills() ->
assert "search-gateway" in cards[0].required_skills
def test_gmp_task_cards_include_fda_enforcement_route() -> None:
brief = {
"research_method": "gmp_quality_operations_diagnosis",
"task_planning": {
"search_routes_by_axis": {
"quality_system_gap": ["fda", "general"],
},
},
}
framework = "## 第1章 偏差和 CAPA 闭环能力决定质量体系可信度\n\n研究思路。"
cards = generate_task_cards_from_research_brief(
"baifan-test",
framework,
brief,
axes=["quality_system_gap"],
)
assert cards[0].search_routes == ["fda", "general"]
assert "FDA Warning Letters" in " ".join(cards[0].questions)
assert "fda_warning_letter_or_meeting_record" in cards[0].expected_evidence["preferred_evidence_types"]
def test_integrated_chapter_mode_is_method_driven_not_gmp_hardcoded() -> None:
brief = {
"research_method": "mckinsey_market",
"phase2_mode": "chapter_integrated",
"task_planning": {},
}
framework = "## 第1章 市场需求正在被支付政策重塑\n\n研究思路。"
cards = generate_task_cards_from_research_brief(
"market-test",
framework,
brief,
)
assert [card.task_id for card in cards] == ["ch01-chapter_integrated"]
assert "literature evidence" in " ".join(cards[0].questions)
assert "FDA Warning Letters" not in " ".join(cards[0].questions)
def test_integrated_task_card_uses_phase1_chapter_planning() -> None:
brief = {
"research_method": "gmp_quality_operations_diagnosis",
"phase2_mode": "chapter_integrated",
"chapter_planning": [
{
"chapter_id": "ch01",
"title": "审计发现应先转化为商业化阶段门缺口",
"core_question": "本章要判断审计发现是否反映阶段门缺口。",
"bold_hypothesis": "大胆假设:风险项计数低估了商业化 readiness 缺口。",
"verification_plan": ["提取现场材料原文", "检索官方法规和执法案例"],
"evidence_lanes": ["site audit findings", "official baseline"],
"minimum_evidence": {"local_material_quotes": 2},
"phase2_prompt_context": "章节:ch01\n必须围绕阶段门缺口求证。",
}
],
"task_planning": {"phase2_mode": "chapter_integrated"},
}
framework = "## 第1章 审计发现应先转化为商业化阶段门缺口\n\n研究思路。"
cards = generate_task_cards_from_research_brief("baifan-test", framework, brief)
assert cards[0].prompt_brief == "章节:ch01\n必须围绕阶段门缺口求证。"
assert cards[0].research_goal == "本章要判断审计发现是否反映阶段门缺口。"
assert "大胆假设:风险项计数低估了商业化 readiness 缺口。" in cards[0].questions
assert cards[0].expected_evidence["phase1_minimum_evidence"] == {"local_material_quotes": 2}
assert cards[0].expected_evidence["must_address_phase1_hypothesis"] is True
assert any("Phase1 的大胆假设" in item for item in cards[0].stop_conditions)
def test_phase1_gmp_hypotheses_are_not_title_restatements(tmp_path: Path) -> None:
project = tmp_path / "baifan"
project.mkdir()
(project / "phase0/extracted").mkdir(parents=True)
material = project / "phase0/extracted/audit.md"
material.write_text(
"人员培训记录齐全,但无菌操作动作违反 First Air 原则,需要进一步培训。\n"
"复盘显示 owner、关闭证据和问题升级机制仍需补齐。\n",
encoding="utf-8",
)
manifest = {
"topic": "白帆生物 GMP 与运营诊断",
"material_inventory": [{"extracted_to": "phase0/extracted/audit.md"}],
}
method = ResearchMethodRegistry().get("gmp_quality_operations_diagnosis")
plans = build_chapter_planning(
project,
manifest,
method,
["人员能力:培训有效性比培训记录更关键", "运营节奏:从临时协调转向管理系统"],
quota=2000,
)
assert "关键解释变量" not in plans[0]["bold_hypothesis"]
assert "不缺培训台账" in plans[0]["bold_hypothesis"]
assert "固定节奏和可视化管理系统" in plans[1]["bold_hypothesis"]
assert plans[0]["core_question"] != plans[0]["title"]
assert "章节成稿应围绕这一观点展开" not in plans[1]["writing_claim"]
def test_research_brief_without_materials_falls_back_to_material_digest() -> None:
brief = {
"research_method": "mckinsey_market",
"phase2_mode": "chapter_integrated",
"phase1_inputs": {"material_digest": "phase1/material_digest.md"},
}
framework = "## 第1章 市场需求正在被支付政策重塑\n\n研究思路。"
cards = generate_task_cards_from_research_brief("market-test", framework, brief)
assert cards[0].allowed_materials == ["phase1/material_digest.md"]
def test_task_card_validation_rejects_duplicates_and_cycles() -> None:
cards = [
TaskCard(task_id="a", chapter_ids=["ch01"], topic_axis="clinical", questions=["q"], search_routes=["scholar"], output_packet="phase2/packets/a.json", dependencies=["b"]),
@@ -154,6 +273,10 @@ def test_packet_validation_requires_sources_and_counter_evidence() -> None:
validate_packet(packet)
packet["source_ids"].append("src_002")
packet["sources"] = [
{"id": "src_001", "title": "来源1", "url": "https://example.com/1"},
{"id": "src_002", "title": "来源2", "url": "https://example.com/2"},
]
validate_packet(packet)
+5
View File
@@ -74,6 +74,10 @@ def valid_response() -> dict:
"evidence_items": [{"source_id": "src_001", "summary": "III 期结果支持主要终点。"}],
"counter_evidence": [{"claim": "长期安全性仍需随访", "source_ids": ["src_002"]}],
"source_ids": ["src_001", "src_002"],
"sources": [
{"id": "src_001", "title": "来源1", "url": "https://example.com/1"},
{"id": "src_002", "title": "来源2", "url": "https://example.com/2"},
],
"source_quality_notes": ["src_001 Tier 1; src_002 Tier 2"],
"open_questions": [],
"raw_quotes_or_notes": ["Original English evidence note is allowed."],
@@ -98,6 +102,7 @@ def test_packet_worker_generates_valid_packet_with_fake_client(tmp_path: Path) -
validate_packet(packet)
assert fake.calls[0]["model"] == role.model
assert "章节证据分析师" in fake.calls[0]["system"]
assert "search-strategy" in fake.calls[0]["system"]