Files
PlantAI-dev/tests/unit/test_interpretation_claim_layers.py
2026-08-03 18:42:47 +08:00

658 lines
28 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"