Files

658 lines
28 KiB
Python
Raw Permalink Normal View History

2026-08-03 18:42:47 +08:00
import json
from pathlib import Path
from gfa_core.mas.agents.interpretation.report_builder import build_claim_layers
from gfa_core.mas.agents.interpretation_evidence import build_evidence_report
from gfa_core.mas.agents.result_parser import ResultParserAgent
def _bundle(stage, parsed=None, evidence=None, risks=None, metrics=None, status="success"):
return {
"stage": stage,
"status": status,
"parsed": parsed or {},
"metrics": metrics or {},
"evidence": evidence or [
{
"stage": stage,
"claim": f"{stage} evidence",
"evidence": f"{stage} local evidence",
"source_type": "manifest",
"source_file": f"{stage}.manifest.json",
"confidence": "high",
"interpretation_level": "fact",
}
],
"risks": risks or [],
}
def test_claim_layers_generate_local_claims_and_audit():
bundles = {
"identify": _bundle("identify", parsed={"final_members": 12, "hmm_hits": 12}),
"domain": _bundle("domain", parsed={"significant_domain_hits": 12, "sbp_protein_count": 12}),
"motif": _bundle("motif", parsed={"motif_count": 10, "most_conserved_motif": {"sites": 12}}),
"promoter": _bundle("promoter", parsed={"genes_with_elements": 12, "top_elements": [{"name": "ABRE", "count": 8}]}),
"expression": _bundle("expression", parsed={"tables": {"gene_exp": {"rows": 12, "columns": 4}}, "top_expression_genes": [{"gene": "FtGene1"}]}),
}
stages = list(bundles)
evidence_cards = [item for stage in stages for item in bundles[stage]["evidence"]]
layers = build_claim_layers(
stages,
bundles,
evidence_cards=evidence_cards,
risk_flags=[],
rag_references={},
scope={"scope_id": "scope.test"},
prefix="Ft.SPL",
)
assert layers["evidence_audit"]["schema"] == "evidence_audit_v1"
assert layers["claims"]
groups = {claim["claim_group"] for claim in layers["claims"]}
assert "family_membership_confidence" in groups
assert "protein_architecture" in groups
assert "candidate_response_gene" in groups
assert all(claim["local_evidence"] for claim in layers["major_claims"])
assert all(claim.get("title") for claim in layers["major_claims"])
assert all(claim.get("interpretation") for claim in layers["major_claims"])
assert all(claim.get("caveat") for claim in layers["major_claims"])
assert all(claim.get("evidence_chain") for claim in layers["major_claims"])
def test_claim_critic_downgrades_high_risk_claims():
high_risk = {"stage": "domain", "severity": "high", "message": "domain统计冲突"}
bundles = {
"domain": _bundle(
"domain",
parsed={"significant_domain_hits": 8, "sbp_protein_count": 8},
risks=[high_risk],
)
}
layers = build_claim_layers(
["domain"],
bundles,
evidence_cards=bundles["domain"]["evidence"],
risk_flags=[high_risk],
rag_references={},
scope={},
prefix="Ft.SPL",
)
claim = layers["claims"][0]
assert claim["confidence"] == "low"
assert any("high" in item for item in claim["limitations"])
def test_build_evidence_report_includes_claim_fields(tmp_path: Path):
job_dir = tmp_path / "job_1"
final_dir = job_dir / "final"
final_dir.mkdir(parents=True)
domtblout = final_dir / "Ft.SPL.domtblout"
domtblout.write_text(
"SBP PF03110.20 100 FtGene1 - 200 1e-20 100 0 1 1 1e-20 1e-20 100 0 1 100 5 104 1 105 0.98 SBP domain\n"
"SBP PF03110.20 100 FtGene2 - 200 1e-18 95 0 1 1 1e-18 1e-18 95 0 1 100 5 104 1 105 0.98 SBP domain\n",
encoding="utf-8",
)
manifests = [
{
"manifest_version": "1.0",
"stage": "identify",
"prefix": "Ft.SPL",
"status": "success",
"metrics": {"final_members": 2, "total_proteins": 100, "hmm_hits": 2, "blast_hits": 2},
"artifacts": {},
},
{
"manifest_version": "1.0",
"stage": "domain",
"prefix": "Ft.SPL",
"status": "success",
"metrics": {"significant_domain_hits": 2, "proteins_total": 2},
"artifacts": {"domtblout": {"path": "final/Ft.SPL.domtblout", "type": "text"}},
},
{
"manifest_version": "1.0",
"stage": "motif",
"prefix": "Ft.SPL",
"status": "success",
"metrics": {"motif_count": 3, "total_sites": 6, "sequence_count": 2, "motifs": [{"id": "motif_1", "sites": 2, "width": 21}]},
"artifacts": {},
},
]
for manifest in manifests:
(final_dir / f"Ft.SPL.{manifest['stage']}.manifest.json").write_text(
json.dumps(manifest, ensure_ascii=False),
encoding="utf-8",
)
report = build_evidence_report(final_dir, "Ft.SPL", job_id="job_1")
assert report is not None
assert report["claims"]
assert report["major_claims"]
assert report["evidence_audit"]["schema"] == "evidence_audit_v1"
assert report["rag_audit"]["status"] == "not_available"
assert "## Major Claims" in report["download_info"]["markdown"]
assert report["key_findings"]
assert report["scientific_discoveries"]
assert report["scientific_discoveries"][0]["interpretation"]
assert report["scientific_discoveries"][0]["caveat"]
assert report["scientific_discoveries"][0]["evidence_chain"]
assert report["evidence_cards"][0]["id"].startswith("E")
assert all(claim.get("evidence_refs") for claim in report["major_claims"])
assert all(item.get("evidence_refs") for item in report["scientific_discoveries"])
assert report["agent_contribution_audit"]["result_agent"]["fact_count"] == len(report["evidence_cards"])
assert report["agent_contribution_audit"]["discovery_agent"]["evidence_bound_claim_count"] >= 1
assert report["agent_contribution_audit"]["rag_agent"]["claim_bound_count"] == 0
assert report["coverage_matrix"]["schema"] == "interpretation_coverage_matrix_v1"
assert report["coverage_matrix"]["summary"]["present_stage_count"] >= 3
assert report["metadata"]["report_kind"] == "route_live"
assert report["metadata"]["completed_expected_stages"]
assert isinstance(report["metadata"]["missing_expected_stages"], list)
structured = report["structured_result_interpretations"]
assert structured["identify"]["result_interpretation"]
assert structured["motif"]["interpretation_depth"] == "article_results_style_stage_interpretation"
assert "3 个 motif" in structured["motif"]["result_interpretation"]
assert "## Structured Result Interpretations" in report["download_info"]["markdown"]
assert "## Scientific Discoveries" in report["download_info"]["markdown"]
assert "## Agent Contribution Audit" in report["download_info"]["markdown"]
assert "## Evidence Cards" in report["download_info"]["markdown"]
def test_claim_layers_generate_evolution_and_property_claims():
bundles = {
"synteny": _bundle("synteny", parsed={"family_pair_count": 2, "synteny_blocks": 316}),
"interspecies_synteny": _bundle("interspecies_synteny", parsed={"family_ref_pair_count": 112, "alignment_blocks": 520}),
"ortholog": _bundle("ortholog", metrics={"ortholog_pairs": 24}),
"kaks": _bundle("kaks", parsed={"successful_pairs": 24, "mean_ka_ks": 1.2, "positive_selection_count": 3}),
"reference_classification": _bundle(
"reference_classification",
metrics={"target_gene_count": 24, "confidence_counts": {"high": 20, "medium": 1, "low": 3}},
),
"subfamily_confirm": _bundle("subfamily_confirm", metrics={"confirmed_genes": 24, "subfamily_counts": {"I": 2, "II": 5}}),
"pep": _bundle("pep", parsed={"protein_count": 24, "avg_length": 410, "avg_pi": 6.5, "avg_gravy": -0.2}),
"ppi": _bundle("ppi", parsed={"genes_in_network": 11, "total_interactions": 27}),
}
stages = list(bundles)
evidence_cards = [item for stage in stages for item in bundles[stage]["evidence"]]
layers = build_claim_layers(
stages,
bundles,
evidence_cards=evidence_cards,
risk_flags=[],
rag_references={},
scope={},
prefix="Ft.SPL",
)
groups = {claim["claim_group"] for claim in layers["claims"]}
assert "evolutionary_duplication" in groups
assert "evolutionary_conservation" in groups
assert "selection_pressure" in groups
assert "subfamily_assignment" in groups
assert "protein_property_profile" in groups
assert "interaction_network" in groups
assert any("24" in claim["claim"] for claim in layers["claims"])
assert any("112" in claim["claim"] for claim in layers["claims"])
def test_failed_subfamily_prediction_generates_low_confidence_reference_claim():
bundles = {
"reference_classification": _bundle(
"reference_classification",
metrics={
"classifiable_species_count": 1,
"failed_species_count": 1,
"per_species_count": 0,
"errors": ["reference sequences matching subfamily table=0"],
},
status="failed",
),
}
layers = build_claim_layers(
["reference_classification"],
bundles,
evidence_cards=bundles["reference_classification"]["evidence"],
risk_flags=[
{
"stage": "reference_classification",
"severity": "high",
"message": "reference_classification 未形成可确认亚家族分类",
}
],
rag_references={},
scope={},
prefix="Ft.SPL",
)
claim = next(item for item in layers["claims"] if item["claim_group"] == "subfamily_prediction_reference")
assert claim["confidence"] == "low"
assert "不能作为目标成员亚家族归属确认" in claim["claim"]
assert "reference sequences matching subfamily table=0" in claim["claim"]
assert claim["counter_evidence"]
def test_claim_level_rag_support_does_not_bind_background_to_kaks():
bundles = {
"kaks": _bundle("kaks", parsed={"successful_pairs": 24, "mean_ka_ks": 0.14, "positive_selection_count": 0}),
}
layers = build_claim_layers(
["kaks"],
bundles,
evidence_cards=bundles["kaks"]["evidence"],
risk_flags=[],
rag_references={
"deep_interpretation": {
"schema": "deep_interpretation_rag_references_v1",
"references": [
{
"claim_id": "selection_pressure.001",
"claim_group": "selection_pressure",
"support_level": "biological_background",
"title": "SPL genes regulate plant development",
"evidence": "SPL/SBP family plays roles in multiple developmental processes.",
}
],
}
},
scope={},
prefix="Ft.SPL",
)
claim = next(item for item in layers["claims"] if item["claim_group"] == "selection_pressure")
assert claim["rag_binding_status"] == "biological_background"
assert claim["rag_interpretation"]["biological_context"]
assert claim["rag_interpretation"]["claim_support"] == []
assert layers["rag_audit"]["claim_binding_count"] == 0
assert layers["rag_audit"]["biological_context_count"] == 1
assert layers["rag_audit"]["status"] == "background_only"
def test_claim_level_rag_support_binds_method_support_to_kaks():
bundles = {
"kaks": _bundle("kaks", parsed={"successful_pairs": 24, "mean_ka_ks": 0.14, "positive_selection_count": 0}),
}
layers = build_claim_layers(
["kaks"],
bundles,
evidence_cards=bundles["kaks"]["evidence"],
risk_flags=[],
rag_references={
"deep_interpretation": {
"schema": "deep_interpretation_rag_references_v1",
"references": [
{
"claim_id": "selection_pressure.001",
"claim_group": "selection_pressure",
"support_level": "method_support",
"title": "dN/dS and Ka/Ks estimate selection pressure",
"evidence": "Ka/Ks and dN/dS are used to screen purifying or positive selection in plant orthologs.",
}
],
}
},
scope={},
prefix="Ft.SPL",
)
claim = next(item for item in layers["claims"] if item["claim_group"] == "selection_pressure")
assert claim["rag_binding_status"] == "method_support"
assert claim["rag_interpretation"]["method_reference"]
assert claim["rag_interpretation"]["claim_support"] == []
assert layers["rag_audit"]["claim_binding_count"] == 0
assert layers["rag_audit"]["method_reference_count"] == 1
assert layers["rag_audit"]["status"] == "method_support"
def test_claim_level_rag_support_counts_only_claim_bound_references():
bundles = {
"kaks": _bundle("kaks", parsed={"successful_pairs": 24, "mean_ka_ks": 0.14, "positive_selection_count": 0}),
}
layers = build_claim_layers(
["kaks"],
bundles,
evidence_cards=bundles["kaks"]["evidence"],
risk_flags=[],
rag_references={
"deep_interpretation": {
"schema": "deep_interpretation_rag_references_v1",
"references": [
{
"claim_id": "selection_pressure.001",
"claim_group": "selection_pressure",
"support_level": "claim_bound",
"title": "Plant gene family Ka/Ks result",
"evidence": "The observed low Ka/Ks values support purifying selection for this family.",
}
],
}
},
scope={},
prefix="Ft.SPL",
)
claim = next(item for item in layers["claims"] if item["claim_group"] == "selection_pressure")
assert claim["rag_binding_status"] == "claim_bound"
assert claim["rag_interpretation"]["claim_support"]
assert layers["rag_audit"]["claim_binding_count"] == 1
assert layers["rag_audit"]["status"] == "claim_bound"
def test_evidence_report_prefers_complete_reference_classification_manifest(tmp_path: Path):
final_dir = tmp_path / "final"
final_dir.mkdir()
scope_id = "SPL.target-Ft.ref-At.target_plus_reference"
member_set_id = "SPL.target-Ft.ref-At.target_plus_reference:subfamily_prediction:all_members"
common = {
"stage": "reference_classification",
"status": "success",
"prefix": "Ft.SPL",
"analysis_route": "subfamily_prediction",
"scope_id": scope_id,
"member_set_id": member_set_id,
}
light = dict(common)
light["metrics"] = {"target_gene_count": 24}
light["artifacts"] = {"report": {"path": "final/light.md"}}
complete = dict(common)
complete["metrics"] = {
"target_gene_count": 24,
"reference_gene_count": 16,
"assigned_subfamily_counts": {"I": 2, "II": 5, "III": 2},
"confidence_counts": {"high": 16, "medium": 5, "low": 3},
}
complete["artifacts"] = {
"assignment_table": {"path": "final/full.assignment.tsv"},
"subfamily_file": {"path": "final/full.subfamily.tsv"},
"confidence_table": {"path": "final/full.confidence.tsv"},
}
(final_dir / "Ft.SPL.target_plus_reference.reference_classification.manifest.json").write_text(json.dumps(light), encoding="utf-8")
(final_dir / "Ft.SPL.SPL.target-Ft.ref-At.target_plus_reference.reference_classification.manifest.json").write_text(json.dumps(complete), encoding="utf-8")
report = build_evidence_report(
final_dir,
"Ft.SPL",
current_stage="reference_classification",
scope_id=scope_id,
analysis_route="subfamily_prediction",
member_set_id=member_set_id,
include_stages=["reference_classification"],
)
assert report is not None
assert report["metadata"]["report_kind"] == "route_live"
assert report["metadata"]["report_mode"] == "subfamily_prediction_report"
assert "亚家族分类预测" in report["metadata"]["report_mode_label"]
assert "不得直接写成家族进化" in report["metadata"]["interpretation_boundary"]
assert report["metadata"]["expected_stages"][-1] == "reference_classification"
assert report["metadata"]["is_final"] is False
claim = next(item for item in report["major_claims"] if item["claim_group"] == "subfamily_assignment")
assert "I:2" in claim["claim"]
assert "high:16" in claim["claim"]
def test_integrated_target_plus_reference_report_does_not_require_reference_classification(tmp_path: Path):
final_dir = tmp_path / "final"
final_dir.mkdir()
scope_id = "SPL.target-Ft.ref-At.target_plus_reference"
for stage in ["identify", "phylogeny", "domain", "motif", "structure", "combo", "interspecies_synteny"]:
manifest = {
"stage": stage,
"status": "success",
"prefix": "Ft.SPL",
"analysis_route": "target_plus_reference",
"scope_id": scope_id,
"metrics": {},
"artifacts": {},
}
if stage == "identify":
manifest["metrics"] = {"final_members": 24}
if stage == "interspecies_synteny":
manifest["metrics"] = {"pair_success_count": 1}
(final_dir / f"Ft.SPL.{scope_id}.{stage}.manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
report = build_evidence_report(
final_dir,
"Ft.SPL",
current_stage="interspecies_synteny",
scope_id=scope_id,
analysis_route="target_plus_reference",
)
assert report is not None
assert report["metadata"]["report_mode"] == "gene_family_integrated_report"
assert "reference_classification" not in report["metadata"]["expected_stages"]
assert "reference_classification" not in report["metadata"]["missing_expected_stages"]
def test_rnaseq_report_uses_rnaseq_mode(tmp_path: Path):
final_dir = tmp_path / "final"
final_dir.mkdir()
for stage, metrics in {
"rnaseq_qc": {"total_samples": 2},
"rnaseq_de": {"de_genes_total": 12, "de_genes_up": 7, "de_genes_down": 5},
}.items():
manifest = {
"stage": stage,
"status": "success",
"prefix": "Os",
"metrics": metrics,
"artifacts": {},
}
if stage == "rnaseq_de":
manifest["rnaseq_scope_id"] = "Os.rnaseq.test"
(final_dir / f"Os.{stage}.manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
report = build_evidence_report(
final_dir,
"Os",
current_stage="rnaseq_de",
job_id="job_rna",
rnaseq_scope_id="Os.rnaseq.test",
)
assert report is not None
assert report["metadata"]["report_mode"] == "rnaseq_integrated_report"
assert report["metadata"]["required_stage_policy"] == "rnaseq_integrated_evidence"
assert "rnaseq_qc" in report["metadata"]["included_stages"]
assert "rnaseq_de" in report["metadata"]["included_stages"]
assert "RNA-seq" in report["metadata"]["report_mode_label"]
assert "调控因果" in report["metadata"]["interpretation_boundary"]
assert "RNA-seq 质控阶段覆盖 2 个样本" in report["structured_result_interpretations"]["rnaseq_qc"]["result_interpretation"]
def test_grouped_interpretation_downgrades_high_gap_and_partial_motif(tmp_path: Path):
final_dir = tmp_path / "final"
final_dir.mkdir()
manifests = [
{
"stage": "phylogeny",
"status": "success",
"prefix": "Si.HDZIP",
"scope_id": "HDZIP.target-Si.ref-At.target_plus_reference",
"analysis_route": "multi_species",
"metrics": {
"sequences_count": 127,
"tree_tip_count": 127,
"gap_percentage": 85.59,
"tree_model": "Q.INSECT+F+R5",
"scope_species_counts": {"Si": 79, "At": 48},
},
"artifacts": {},
},
{
"stage": "domain",
"status": "success",
"prefix": "Si.HDZIP",
"scope_id": "HDZIP.target-Si.ref-At.target_plus_reference",
"analysis_route": "subfamily_evidence",
"metrics": {
"proteins_total": 127,
"proteins_with_domains": 127,
"significant_domain_hits": 273,
"top_domains": [
{"domain": "Homeodomain", "count": 125},
{"domain": "HALZ", "count": 45},
{"domain": "START", "count": 40},
],
},
"artifacts": {},
},
{
"stage": "motif",
"status": "partial_success",
"prefix": "Si.HDZIP",
"scope_id": "HDZIP.target-Si.ref-At.target_plus_reference",
"analysis_route": "subfamily_evidence",
"metrics": {
"sequence_count": 127,
"motif_count": 10,
"total_sites": 215,
"motifs": [{"id": "motif_1", "sites": 27, "width": 82}],
},
"parameters": {"stage_status": {"meme": True, "summary": True, "seqlogo": False, "tree_motif": True}},
"artifacts": {},
},
{
"stage": "structure",
"status": "success",
"prefix": "Si.HDZIP",
"scope_id": "HDZIP.target-Si.ref-At.target_plus_reference",
"analysis_route": "subfamily_evidence",
"metrics": {"genes_total": 127, "genes_with_structure": 127, "avg_exon_count_per_gene": 4.2},
"artifacts": {},
},
]
for manifest in manifests:
manifest["manifest_version"] = "1.0"
(final_dir / f"Si.HDZIP.{manifest['stage']}.manifest.json").write_text(
json.dumps(manifest, ensure_ascii=False),
encoding="utf-8",
)
report = build_evidence_report(
final_dir,
"Si.HDZIP",
current_stage="structure",
scope_id="HDZIP.target-Si.ref-At.target_plus_reference",
analysis_route="target_plus_reference",
)
assert report is not None
structured = report["structured_result_interpretations"]
assert structured["phylogeny"]["schema"] == "structured_result_interpretation_v1"
assert structured["phylogeny"]["stage_group"] == "classification_structure_support"
assert structured["phylogeny"]["interpretation_level"] == "descriptive_only"
assert any("gap比例较高" in reason for reason in structured["phylogeny"]["downgrade_reasons"])
assert structured["motif"]["interpretation_level"] == "descriptive_only"
assert any("partial_success" in reason for reason in structured["motif"]["downgrade_reasons"])
assert structured["motif"]["missing_slots"]
grouped = report["grouped_result_interpretations"]["classification_structure_support"]
assert grouped["schema"] == "grouped_result_interpretation_v1"
assert grouped["readiness"] == "descriptive_only"
assert "127 条序列" in grouped["result_interpretation"]
assert "Homeodomain" in grouped["result_interpretation"]
assert "强拓扑结论" in grouped["boundary"]
assert "## Grouped Results Interpretation" in report["download_info"]["markdown"]
assert report["evidence_bundle"]["discovery_agent"]["grouped_result_interpretations"]["classification_structure_support"]["readiness"] == "descriptive_only"
def test_interspecies_synteny_uses_pairwise_collinearity_artifact(tmp_path: Path):
job_dir = tmp_path / "job_1"
final_dir = job_dir / "final"
analysis_dir = job_dir / "interspecies_synteny" / "analysis"
final_dir.mkdir(parents=True)
analysis_dir.mkdir(parents=True)
family_fasta = final_dir / "Ft.SPL.final_members.pep.fa"
family_fasta.write_text(">FtPinG0000010000.01.T01\nMA\n", encoding="utf-8")
ref_fasta = analysis_dir / "Ft.SPL.selected_ref.family.pep.fa"
ref_fasta.write_text(">AT1G01010.1\nMA\n", encoding="utf-8")
collinearity = analysis_dir / "Ft.At.collinearity"
collinearity.write_text(
"## Alignment 0: score=100 e_value=1e-10 N=1 1&Ft1 plus\n"
" 0- 0:\tAT1G01010.1\tFtPinG0000010000.01.T01\t1e-50\n",
encoding="utf-8",
)
identify_manifest = {
"stage": "identify",
"status": "success",
"prefix": "Ft.SPL",
"metrics": {"final_members": 1},
"artifacts": {"final_members": {"path": "final/Ft.SPL.final_members.pep.fa"}},
}
inter_manifest = {
"stage": "interspecies_synteny",
"status": "success",
"prefix": "Ft.SPL",
"analysis_route": "multi_species",
"metrics": {
"pairwise_synteny": [
{
"pair": "Ft.At",
"anchor_count": 1,
"family_highlight_links": 1,
"marked_block_count": 1,
}
]
},
"parameters": {"ref_protein_spl": str(ref_fasta)},
"artifacts": {"Ft.At_collinearity": {"full_path": str(collinearity)}},
}
(final_dir / "Ft.SPL.identify.manifest.json").write_text(json.dumps(identify_manifest), encoding="utf-8")
(final_dir / "Ft.SPL.interspecies_synteny.manifest.json").write_text(json.dumps(inter_manifest), encoding="utf-8")
report = build_evidence_report(
final_dir,
"Ft.SPL",
current_stage="interspecies_synteny",
analysis_route="multi_species",
)
assert report is not None
review = next(item for item in report["stage_reviews"] if item["stage"] == "interspecies_synteny")
evidence = next(item for item in report["evidence_cards"] if item["stage"] == "interspecies_synteny" and item["claim"] == "种间共线性原始解析")
assert "全基因组种间block为1" in ";".join(review["key_points"])
assert "1对目标-参考家族共线性" in report["stage_flow"]["stages"][-1]["key_result"]
assert report["source_summary"]["high_risk_count"] == 0
assert "图中家族高亮连线1条" in evidence["evidence"]
def test_interpretation_index_uses_report_metadata(tmp_path: Path):
workspace_dir = tmp_path / "final"
workspace_dir.mkdir()
manifest = workspace_dir / "Ft.SPL.identify.manifest.json"
manifest.write_text(json.dumps({"stage": "identify", "status": "success"}), encoding="utf-8")
report = {
"metadata": {
"prefix": "Ft.SPL",
"scope_id": "SPL.target-Ft.target_only",
"analysis_route": "single_species",
"included_stages": ["identify", "pep"],
"stage_count": 2,
"generated_at": "2026-06-23T02:10:00",
},
"source_summary": {"claim_count": 4, "major_claim_count": 3},
"evidence_bundle": {
"result_agent": {
"stage_status": [{"stage": "identify", "manifest_path": str(manifest)}]
}
},
"claims": [1, 2, 3, 4],
"major_claims": [1, 2, 3],
}
agent = ResultParserAgent(None)
identity = agent._interpretation_identity("Ft.SPL", report)
cache_path = workspace_dir / agent._interpretation_cache_name(identity)
cache_path.write_text(json.dumps(report), encoding="utf-8")
agent._update_interpretation_index(workspace_dir, cache_path, identity, report)
index = json.loads((workspace_dir / "interpretation.index.json").read_text(encoding="utf-8"))
entry = index["entries"][0]
assert entry["stage_count"] == 2
assert entry["included_stages"] == ["identify", "pep"]
assert entry["claim_count"] == 4
assert entry["major_claim_count"] == 3
assert entry["analysis_route"] == "single_species"