fix: integrate deployed reports with PPI KaKs handoff
This commit is contained in:
Executable
+66
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate or open a formal RNA-seq evidence report."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).absolute().parents[2]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from gfa_core.rnaseq_report import NoSuccessfulRNASeqEvidence, generate_rnaseq_report
|
||||
|
||||
|
||||
def _csv(value: str) -> list[str] | None:
|
||||
values = [item.strip() for item in str(value or "").split(",") if item.strip()]
|
||||
return values or None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate a deterministic formal RNA-seq result report from successful manifests."
|
||||
)
|
||||
parser.add_argument("--workspace", required=True, help="RNA-seq job workspace containing final/")
|
||||
parser.add_argument("--prefix", required=True, help="RNA-seq output prefix")
|
||||
parser.add_argument("--job-id", default="")
|
||||
parser.add_argument("--rnaseq-scope-id", default="")
|
||||
parser.add_argument(
|
||||
"--expected-stages",
|
||||
default="",
|
||||
help="Comma-separated stages selected for the current RNA-seq batch",
|
||||
)
|
||||
parser.add_argument("--action", choices=("generate", "open"), default="generate")
|
||||
parser.add_argument("--no-model", action="store_true", help="Disable optional LLM language enhancement")
|
||||
parser.add_argument("--log-file", default="", help="Accepted for pipeline runner compatibility")
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
result = generate_rnaseq_report(
|
||||
Path(args.workspace),
|
||||
args.prefix,
|
||||
rnaseq_scope_id=args.rnaseq_scope_id,
|
||||
expected_stages=_csv(args.expected_stages),
|
||||
use_model=not args.no_model,
|
||||
action=args.action,
|
||||
job_id=args.job_id,
|
||||
)
|
||||
except NoSuccessfulRNASeqEvidence as exc:
|
||||
print(str(exc), file=sys.stderr)
|
||||
return 2
|
||||
except Exception as exc:
|
||||
print(f"RNA-seq 结果报告生成失败:{type(exc).__name__}: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
reuse = "(复用最新报告)" if result.reused else ""
|
||||
print(
|
||||
f"RNA-seq 结果报告已就绪{reuse}: {result.report_path} "
|
||||
f"[coverage={result.coverage_status}, stages={','.join(result.included_stages)}]"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -3412,6 +3412,7 @@ RNA_SEQ_STAGES = {
|
||||
"rnaseq_wgcna",
|
||||
"rnaseq_candidate_family",
|
||||
"rnaseq_family_score",
|
||||
"rnaseq_report",
|
||||
}
|
||||
|
||||
|
||||
@@ -8441,7 +8442,32 @@ def run_stage(
|
||||
expected_stages = [str(value).strip() for value in expected_stages if str(value).strip()]
|
||||
report_action = str(kwargs.get("report_action") or kwargs.get("reportAction") or "generate").strip()
|
||||
if report_action == "generate" and not expected_stages:
|
||||
return "❌ 基因家族结果报告生成需要非空 expected_stages", manifest
|
||||
report_stage_order = (
|
||||
"identify", "phylogeny", "reference_classification", "motif",
|
||||
"domain", "structure", "combo", "promoter", "tfbs",
|
||||
"synteny", "karyotype", "interspecies_synteny", "pep",
|
||||
)
|
||||
recovered_stages = set()
|
||||
for candidate in final_dir.glob("*.manifest.json"):
|
||||
try:
|
||||
payload = json.loads(candidate.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
continue
|
||||
candidate_stage = str(payload.get("stage") or "").strip()
|
||||
candidate_prefix = str(
|
||||
payload.get("prefix")
|
||||
or (payload.get("parameters") or {}).get("prefix")
|
||||
or ""
|
||||
).strip()
|
||||
if (
|
||||
candidate_stage in report_stage_order
|
||||
and str(payload.get("status") or "") == "success"
|
||||
and candidate_prefix == prefix
|
||||
):
|
||||
recovered_stages.add(candidate_stage)
|
||||
expected_stages = [stage_id for stage_id in report_stage_order if stage_id in recovered_stages]
|
||||
if report_action == "generate" and not expected_stages:
|
||||
return "❌ 基因家族结果报告生成需要非空 expected_stages,且当前前缀没有可恢复的成功 manifest", manifest
|
||||
stage_statuses = kwargs.get("stage_statuses") or kwargs.get("stageStatuses") or {}
|
||||
if isinstance(stage_statuses, str):
|
||||
try:
|
||||
@@ -8450,6 +8476,8 @@ def run_stage(
|
||||
return "❌ 基因家族结果报告的 stage_statuses 不是合法 JSON", manifest
|
||||
if not isinstance(stage_statuses, dict):
|
||||
return "❌ 基因家族结果报告的 stage_statuses 必须是对象", manifest
|
||||
if not stage_statuses:
|
||||
stage_statuses = {stage_id: "success" for stage_id in expected_stages}
|
||||
report_use_model_raw = kwargs.get("report_use_model", kwargs.get("reportUseModel", True))
|
||||
rnaseq_scope_id = str(kwargs.get("rnaseq_scope_id") or kwargs.get("rnaseqScopeId") or "").strip()
|
||||
cmd = [
|
||||
@@ -11465,6 +11493,42 @@ def run_stage(
|
||||
return "✅ RNA-seq WGCNA completed.", wgcna_manifest
|
||||
return f"❌ RNA-seq WGCNA failed (exit code: {code}). Log: {log_file}", wgcna_manifest if wgcna_manifest.exists() else manifest
|
||||
|
||||
if stage == "rnaseq_report":
|
||||
report_action = str(kwargs.get("rnaseq_report_action") or kwargs.get("report_action") or "generate").strip().lower()
|
||||
if report_action not in {"generate", "open"}:
|
||||
report_action = "generate"
|
||||
expected_raw = kwargs.get("rnaseq_expected_stages") or kwargs.get("expected_stages") or ""
|
||||
if isinstance(expected_raw, (list, tuple, set)):
|
||||
expected_arg = ",".join(str(item).strip() for item in expected_raw if str(item).strip())
|
||||
else:
|
||||
expected_arg = str(expected_raw or "").strip()
|
||||
report_scope_id = str(kwargs.get("rnaseq_scope_id") or kwargs.get("rnaseqScopeId") or "").strip()
|
||||
report_use_model = kwargs.get("rnaseq_report_use_model", kwargs.get("report_use_model", True))
|
||||
report_cmd = [
|
||||
sys.executable,
|
||||
"apps/cli/run_rnaseq_report.py",
|
||||
"--workspace", str(ws),
|
||||
"--prefix", prefix,
|
||||
"--job-id", job.job_id,
|
||||
"--action", report_action,
|
||||
"--log-file", str(log_file),
|
||||
]
|
||||
if report_scope_id:
|
||||
report_cmd.extend(["--rnaseq-scope-id", report_scope_id])
|
||||
if expected_arg:
|
||||
report_cmd.extend(["--expected-stages", expected_arg])
|
||||
if not _truthy(report_use_model):
|
||||
report_cmd.append("--no-model")
|
||||
print(f"[CMD] {' '.join(report_cmd)}")
|
||||
code = run_cmd_logged(report_cmd, log_file, cwd=Path("."))
|
||||
report_manifest = final_dir / f"{prefix}.rnaseq_report.manifest.json"
|
||||
if code == 0 and report_manifest.exists():
|
||||
action_label = "打开" if report_action == "open" else "生成"
|
||||
return f"✅ RNA-seq 结果报告已{action_label}。日志:{log_file}", report_manifest
|
||||
if code == 2:
|
||||
return "⚠️ 暂无可生成的 RNA-seq 结果:当前 prefix/scope 下没有成功阶段。", None
|
||||
return f"❌ RNA-seq 结果报告生成失败(exit code: {code})。日志:{log_file}", report_manifest if report_manifest.exists() else None
|
||||
|
||||
if stage == "rnaseq_candidate_family":
|
||||
_SPECIES_RESOURCE_ALIASES = {
|
||||
"os": ["Os", "Oryza", "Oryza_sativa", "rice"],
|
||||
|
||||
+938
-212
File diff suppressed because it is too large
Load Diff
+80
-5
@@ -4746,7 +4746,7 @@ def _celery_start_script_for_current_profile() -> Path:
|
||||
port = os.environ.get("PLANTAI_WEB_PORT", "").strip()
|
||||
if prefix.endswith("_9000") or redis_url.endswith("/10") or port == "9000":
|
||||
return PROJECT_ROOT / "start_celery_9000.sh"
|
||||
return PROJECT_ROOT / "start_celery_7880.sh"
|
||||
return PROJECT_ROOT / "scripts" / "legacy_startup" / "start_celery_7880.sh"
|
||||
|
||||
|
||||
def _current_celery_profile() -> Dict[str, Any]:
|
||||
@@ -4756,7 +4756,11 @@ def _current_celery_profile() -> Dict[str, Any]:
|
||||
prefix = get_queue_prefix()
|
||||
except Exception:
|
||||
prefix = os.environ.get("BBAV5_QUEUE_PREFIX", "plantai_dev_7880").strip() or "plantai_dev_7880"
|
||||
redis_default = "redis://localhost:6379/10" if prefix.endswith("_9000") or os.environ.get("PLANTAI_WEB_PORT", "").strip() == "9000" else "redis://localhost:6379/9"
|
||||
current_port = os.environ.get("PLANTAI_WEB_PORT", "").strip()
|
||||
if prefix.endswith("_9000") or current_port == "9000":
|
||||
redis_default = "redis://localhost:6379/10"
|
||||
else:
|
||||
redis_default = "redis://localhost:6379/9"
|
||||
redis_url = os.environ.get("BBAV5_REDIS_URL", redis_default).strip() or redis_default
|
||||
script = _celery_start_script_for_current_profile()
|
||||
log_dir = Path(os.environ.get("BBAV5_CELERY_LOG_DIR", "") or f"logs/celery_{prefix}")
|
||||
@@ -8647,7 +8651,7 @@ def run_stage_sync(task_id: str, stage: str, job_id: str, params: dict):
|
||||
async def root():
|
||||
"""返回Vue3前端页面"""
|
||||
return FileResponse(
|
||||
Path(__file__).parent / "index.html",
|
||||
Path(__file__).parent / "index_ml.html",
|
||||
headers={
|
||||
"Cache-Control": "no-cache, no-store, must-revalidate",
|
||||
"Pragma": "no-cache",
|
||||
@@ -17217,7 +17221,78 @@ def _gene_family_report_plan_context(job_id: str, prefix: str = "") -> Optional[
|
||||
"rnaseqScopeId": "",
|
||||
}
|
||||
reports.append((path.stat().st_mtime, context))
|
||||
return max(reports, key=lambda item: item[0])[1] if reports else None
|
||||
if reports:
|
||||
return max(reports, key=lambda item: item[0])[1]
|
||||
|
||||
# Older jobs may have complete, successful stage manifests but no saved
|
||||
# confirmation-plan snapshot. Reconstruct a deterministic report plan from
|
||||
# one manifest prefix rather than rejecting those verifiable results.
|
||||
report_stage_order = (
|
||||
"identify",
|
||||
"phylogeny",
|
||||
"reference_classification",
|
||||
"motif",
|
||||
"domain",
|
||||
"structure",
|
||||
"combo",
|
||||
"promoter",
|
||||
"tfbs",
|
||||
"synteny",
|
||||
"karyotype",
|
||||
"interspecies_synteny",
|
||||
"pep",
|
||||
)
|
||||
report_stage_set = set(report_stage_order)
|
||||
recovered_by_prefix: Dict[str, Dict[str, tuple[float, Dict[str, Any]]]] = {}
|
||||
for path in final_dir.glob("*.manifest.json") if final_dir.is_dir() else ():
|
||||
try:
|
||||
manifest = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
continue
|
||||
stage = str(manifest.get("stage") or "").strip()
|
||||
manifest_prefix = str(
|
||||
manifest.get("prefix")
|
||||
or (manifest.get("parameters") or {}).get("prefix")
|
||||
or ""
|
||||
).strip()
|
||||
if (
|
||||
stage not in report_stage_set
|
||||
or str(manifest.get("status") or "") != "success"
|
||||
or not manifest_prefix
|
||||
or (requested_prefix and manifest_prefix != requested_prefix)
|
||||
):
|
||||
continue
|
||||
stage_map = recovered_by_prefix.setdefault(manifest_prefix, {})
|
||||
candidate = (path.stat().st_mtime, manifest)
|
||||
if stage not in stage_map or candidate[0] > stage_map[stage][0]:
|
||||
stage_map[stage] = candidate
|
||||
|
||||
if not recovered_by_prefix:
|
||||
return None
|
||||
|
||||
def recovered_rank(item: tuple[str, Dict[str, tuple[float, Dict[str, Any]]]]) -> tuple[int, int, float, str]:
|
||||
manifest_prefix, stage_map = item
|
||||
latest_mtime = max((entry[0] for entry in stage_map.values()), default=0.0)
|
||||
return (
|
||||
1 if "identify" in stage_map else 0,
|
||||
len(stage_map),
|
||||
latest_mtime,
|
||||
manifest_prefix,
|
||||
)
|
||||
|
||||
recovered_prefix, recovered_stages = max(recovered_by_prefix.items(), key=recovered_rank)
|
||||
planned = [stage for stage in report_stage_order if stage in recovered_stages]
|
||||
if not planned:
|
||||
return None
|
||||
return {
|
||||
"prefix": recovered_prefix,
|
||||
"expectedStages": planned,
|
||||
"stageStatuses": {stage: "success" for stage in planned},
|
||||
"scopeId": "",
|
||||
"analysisRoute": "",
|
||||
"memberSetId": "",
|
||||
"rnaseqScopeId": "",
|
||||
}
|
||||
|
||||
|
||||
async def _run_agent_chat(
|
||||
@@ -25417,7 +25492,7 @@ async def root():
|
||||
"""
|
||||
返回前端首页
|
||||
"""
|
||||
index_file = WEB_DIR / "index.html"
|
||||
index_file = WEB_DIR / "index_ml.html"
|
||||
if index_file.exists():
|
||||
return FileResponse(
|
||||
str(index_file),
|
||||
|
||||
@@ -63,7 +63,7 @@ env BBAV5_QUEUE_PREFIX=plantai_dev_7908 \
|
||||
BBAV5_REDIS_URL=redis://localhost:6379/9 \
|
||||
BBAV5_CELERY_LOG_DIR=logs/celery_plantai_dev_7908 \
|
||||
BBAV5_WORKER_NAME_PREFIX=plantai_dev_7908 \
|
||||
./start_celery_7880.sh
|
||||
./start_celery_9000.sh
|
||||
|
||||
env BBAV5_QUEUE_PREFIX=plantai_dev_7908 \
|
||||
BBAV5_REDIS_URL=redis://localhost:6379/9 \
|
||||
|
||||
@@ -700,11 +700,14 @@ def build_report_data(
|
||||
),
|
||||
]
|
||||
sections.extend(stage_sections)
|
||||
# The integrated conclusion has a fixed evidence contract. Do not take the
|
||||
# first arbitrary observations because plan ordering can displace one of
|
||||
# the three core facts requested for delivery.
|
||||
integrated_observations = [
|
||||
section.observations[0]
|
||||
for section in stage_sections
|
||||
if section.observations
|
||||
][:3]
|
||||
stage_sections_by_name[stage].observations[0]
|
||||
for stage in ("identify", "phylogeny", "reference_classification")
|
||||
if stage in stage_sections_by_name and stage_sections_by_name[stage].observations
|
||||
]
|
||||
sections.extend(
|
||||
[
|
||||
ReportSection(
|
||||
|
||||
@@ -52,6 +52,7 @@ STAGE_SCRIPT_MAP = {
|
||||
"annotation_kegg": "run_annotation.py",
|
||||
"rnaseq_wgcna": "run_rnaseq_wgcna.py",
|
||||
"rnaseq_candidate_family": "run_rnaseq_candidate_family.py",
|
||||
"rnaseq_report": "run_rnaseq_report.py",
|
||||
"rnaseq_sample_qc": "run_rnaseq_sample_qc.py",
|
||||
"genome_prepare": "run_genome_prepare.py",
|
||||
"genome_prepare_v2": "run_genome_prepare.py",
|
||||
@@ -341,6 +342,11 @@ DIAGNOSIS_STAGE_COVERAGE = {
|
||||
"postflight": ["candidate_family_ranking", "evidence_matrix", "recommendation", "rag_queries", "rag_references"],
|
||||
"failure": ["annotation_missing", "id_mapping"],
|
||||
},
|
||||
"rnaseq_report": {
|
||||
"preflight": ["successful_rnaseq_manifest", "prefix_scope_identity"],
|
||||
"postflight": ["report_markdown", "evidence_snapshot", "coverage_status"],
|
||||
"failure": ["no_successful_rnaseq_evidence", "scope_mismatch", "artifact_missing"],
|
||||
},
|
||||
"annotation": {
|
||||
"preflight": ["protein_background", "tool_availability"],
|
||||
"postflight": ["gene2go", "gene2ko", "annotation_coverage"],
|
||||
|
||||
@@ -74,6 +74,7 @@ class IntentAgentV2(BaseAgent):
|
||||
"rnaseq_de": "RNA-seq差异表达分析 - 从原始FASTQ数据开始,进行质控→比对→定量→差异表达分析(DESeq2)",
|
||||
"rnaseq_enrich": "RNA-seq富集分析 - GO/KEGG通路富集分析(RNA-seq流程最后一步)",
|
||||
"rnaseq_candidate_family": "RNA-seq候选家族筛选 - 整合DEG、InterPro/Pfam和GO/KEGG富集,输出候选基因家族排名",
|
||||
"rnaseq_report": "RNA-seq结果报告 - 从当前成功阶段生成可追溯的正式Markdown报告",
|
||||
"enrichment": "富集分析 - 基于基因家族鉴定结果进行GO/KEGG通路富集分析",
|
||||
"ncbi_fetch_sequences": "NCBI序列下载 - 根据GeneID、基因名或基因list下载protein/CDS序列"
|
||||
}
|
||||
@@ -521,6 +522,22 @@ class IntentAgentV2(BaseAgent):
|
||||
has_rnaseq_marker = any(kw in query_lower for kw in ["rnaseq", "rna-seq", "rna seq", "转录组"])
|
||||
explicit_gene_family_context = any(kw in query_lower for kw in ["基因家族", "家族表达", "表达量", "热图", "heatmap"])
|
||||
|
||||
if has_rnaseq_marker and any(
|
||||
keyword in query_lower
|
||||
for keyword in ["生成结果报告", "打开结果报告", "结果报告", "生成报告", "打开报告"]
|
||||
):
|
||||
action = "open" if "打开" in query_lower else "generate"
|
||||
return {
|
||||
"gene_family": "",
|
||||
"species": "",
|
||||
"stages": ["rnaseq_report"],
|
||||
"primary_stage": "rnaseq_report",
|
||||
"description": "打开或生成当前RNA-seq正式结果报告",
|
||||
"requirements": ["至少一个成功的RNA-seq阶段manifest"],
|
||||
"params": {"rnaseqReportAction": action},
|
||||
"confidence": 0.99,
|
||||
}
|
||||
|
||||
stage_force_rules = [
|
||||
("rnaseq_candidate_family", ["候选家族", "候选基因家族", "家族排名", "tf注释", "tf family", "tffamily", "pfam注释", "deg注释", "差异基因注释"], "执行RNA-seq候选家族筛选"),
|
||||
("rnaseq_fgsea", ["fgsea", "gsea", "排序富集", "基因集富集"], "执行RNA-seq fgsea排序富集分析"),
|
||||
|
||||
@@ -376,6 +376,7 @@ class PlanningAgentV2(BaseAgent):
|
||||
"rnaseq_fgsea": [],
|
||||
"rnaseq_wgcna": [],
|
||||
"rnaseq_candidate_family": [],
|
||||
"rnaseq_report": [],
|
||||
}
|
||||
|
||||
# 各阶段依赖 (来自PreFlight)
|
||||
|
||||
@@ -20,6 +20,7 @@ ANALYSIS_KEYWORDS = {
|
||||
"rnaseq_kegg_enrich": ["kegg富集", "kegg分析", "kegg", "pathway"],
|
||||
"rnaseq_fgsea": ["fgsea", "gsea", "排序富集", "基因集富集"],
|
||||
"rnaseq_candidate_family": ["候选家族", "候选基因家族", "家族排名", "tf注释", "tf family", "tffamily", "pfam注释", "deg注释", "差异基因注释"],
|
||||
"rnaseq_report": ["rnaseq结果报告", "rna-seq结果报告", "转录组结果报告", "rnaseq报告", "rna-seq报告"],
|
||||
"rnaseq_enrich": ["富集分析", "功能富集"],
|
||||
"identify": ["鉴定", "identify", "识别", "找出", "鉴定基因家族"],
|
||||
"identify_refine": ["修订成员", "修订鉴定成员", "修改成员", "删除基因", "删除", "去除基因", "去掉基因", "移除基因", "补充基因", "遗漏基因", "重建蛋白", "人工修订id", "curated members", "refine identify"],
|
||||
@@ -61,6 +62,7 @@ RNASEQ_STAGES = {
|
||||
"rnaseq_kegg_enrich",
|
||||
"rnaseq_fgsea",
|
||||
"rnaseq_candidate_family",
|
||||
"rnaseq_report",
|
||||
"rnaseq_enrich",
|
||||
}
|
||||
|
||||
@@ -179,6 +181,21 @@ def fast_intent_recognition(query: str) -> Optional[Dict[str, Any]]:
|
||||
except Exception:
|
||||
pass
|
||||
has_rnaseq_marker = any(marker in query_lower for marker in RNASEQ_MARKERS)
|
||||
if has_rnaseq_marker and any(
|
||||
keyword in query_lower
|
||||
for keyword in ["生成结果报告", "打开结果报告", "结果报告", "生成报告", "打开报告"]
|
||||
):
|
||||
action = "open" if "打开" in query_lower else "generate"
|
||||
return {
|
||||
"stage": "rnaseq_report",
|
||||
"stages": ["rnaseq_report"],
|
||||
"gene_family": "",
|
||||
"species": "",
|
||||
"prefix": "",
|
||||
"params": {"rnaseqReportAction": action},
|
||||
"confidence": 0.99,
|
||||
"method": "rnaseq_report_deterministic",
|
||||
}
|
||||
header_clean_context = any(kw in query_lower for kw in [
|
||||
"id处理", "处理id", "修订id", "简化id", "清理id", "id简化",
|
||||
"去掉注释", "去除注释", "注释信息", "长注释",
|
||||
|
||||
@@ -793,6 +793,26 @@ CAPABILITIES: Dict[str, StageCapability] = {
|
||||
repair_candidates=["gff_id_overlap_report", "motif_database_fallback_to_jaspar", "artifact_reresolve"],
|
||||
rerun_policy="manual",
|
||||
),
|
||||
"rnaseq_report": StageCapability(
|
||||
stage="rnaseq_report",
|
||||
cli="apps/cli/run_rnaseq_report.py",
|
||||
env="test4py311",
|
||||
category="rnaseq",
|
||||
consumes=[
|
||||
ArtifactNeed("successful_rnaseq_manifests", "manifest", alternatives=["final_manifest_scan"]),
|
||||
],
|
||||
provides=[
|
||||
ArtifactProduct("rnaseq_report", "markdown", "{prefix}.rnaseq_report.md"),
|
||||
ArtifactProduct("rnaseq_report_evidence", "json", "{prefix}.rnaseq_report.evidence.json"),
|
||||
ArtifactProduct("rnaseq_report_model", "json", "{prefix}.rnaseq_report.model.json"),
|
||||
ArtifactProduct("rnaseq_report_manifest", "manifest", "{prefix}.rnaseq_report.manifest.json"),
|
||||
],
|
||||
scope_modes=["rnaseq_scope_id"],
|
||||
preflight_checks=["at_least_one_successful_rnaseq_manifest", "prefix_scope_identity_consistent"],
|
||||
postflight_checks=["deterministic_markdown_exists", "evidence_snapshot_exists", "coverage_recorded"],
|
||||
repair_candidates=["manifest_recovery", "regenerate_from_current_snapshot"],
|
||||
rerun_policy="manual_or_after_selected_batch_terminal",
|
||||
),
|
||||
"summary": StageCapability(
|
||||
stage="summary",
|
||||
cli="apps/cli/run_summary.py",
|
||||
@@ -804,11 +824,14 @@ CAPABILITIES: Dict[str, StageCapability] = {
|
||||
],
|
||||
provides=[
|
||||
ArtifactProduct("summary_report", "markdown", "{prefix}.summary_report.md"),
|
||||
ArtifactProduct("summary_evidence_index", "markdown", "{prefix}.summary_evidence_index.md"),
|
||||
ArtifactProduct("summary_evidence", "json", "{prefix}.summary.evidence.json"),
|
||||
ArtifactProduct("deepseek_metadata", "json", "{prefix}.summary.deepseek.json"),
|
||||
ArtifactProduct("summary_manifest", "manifest", "{prefix}.summary.manifest.json"),
|
||||
],
|
||||
scope_modes=["target_only", "target_plus_reference", "all_selected", "rnaseq"],
|
||||
preflight_checks=["at_least_one_success_manifest"],
|
||||
postflight_checks=["summary_contains_stage_evidence"],
|
||||
postflight_checks=["summary_contains_stage_evidence", "deepseek_report_generation_proven"],
|
||||
repair_candidates=["manifest_recovery", "artifact_reresolve"],
|
||||
rerun_policy="auto_after_manifest_recovery",
|
||||
),
|
||||
|
||||
@@ -90,6 +90,7 @@ PIPELINE_STAGES = {
|
||||
"rnaseq_go_enrich",
|
||||
"rnaseq_kegg_enrich",
|
||||
"rnaseq_candidate_family",
|
||||
"rnaseq_report",
|
||||
}
|
||||
|
||||
STAGE_DEPENDENCIES = {
|
||||
@@ -173,6 +174,9 @@ STAGE_DEPENDENCIES = {
|
||||
"rnaseq_go_enrich": ["rnaseq_de"],
|
||||
"rnaseq_kegg_enrich": ["rnaseq_de"],
|
||||
"rnaseq_candidate_family": ["rnaseq_de", "rnaseq_go_enrich", "rnaseq_kegg_enrich", "rnaseq_fgsea"],
|
||||
# A report is a snapshot of whatever RNA-seq evidence is already successful.
|
||||
# Static dependencies would incorrectly prevent intentional mid-pipeline reports.
|
||||
"rnaseq_report": [],
|
||||
}
|
||||
|
||||
PARALLEL_GROUPS = [
|
||||
@@ -271,6 +275,7 @@ STAGE_RESOURCE_PROFILES = {
|
||||
"rnaseq_kegg_enrich": {"queue": "rnaseq", "resource_class": "rnaseq", "default_cpu": 4, "max_cpu": 4},
|
||||
"rnaseq_fgsea": {"queue": "rnaseq", "resource_class": "rnaseq", "default_cpu": 4, "max_cpu": 4},
|
||||
"rnaseq_candidate_family": {"queue": "rnaseq", "resource_class": "rnaseq", "default_cpu": 4, "max_cpu": 4},
|
||||
"rnaseq_report": {"queue": "light", "resource_class": "light", "default_cpu": 4, "max_cpu": 4},
|
||||
}
|
||||
|
||||
DEFAULT_STAGE_RESOURCE_PROFILE = {
|
||||
@@ -391,6 +396,7 @@ STAGE_ORDER = [
|
||||
"rnaseq_kegg_enrich",
|
||||
"rnaseq_fgsea",
|
||||
"rnaseq_candidate_family",
|
||||
"rnaseq_report",
|
||||
"rnaseq_enrich",
|
||||
"gene_family_report",
|
||||
]
|
||||
@@ -469,6 +475,7 @@ STAGE_INFO = {
|
||||
"rnaseq_enrich": {"name": "RNA-seq富集分析", "estimated_time": 15, "cpu": 4},
|
||||
"rnaseq_fgsea": {"name": "RNA-seq fgsea分析", "estimated_time": 15, "cpu": 4},
|
||||
"rnaseq_candidate_family": {"name": "RNA-seq候选家族筛选", "estimated_time": 20, "cpu": 4},
|
||||
"rnaseq_report": {"name": "RNA-seq结果报告", "estimated_time": 2, "cpu": 4},
|
||||
}
|
||||
|
||||
EXECUTABLE_STAGES = {
|
||||
@@ -544,6 +551,7 @@ EXECUTABLE_STAGES = {
|
||||
"rnaseq_enrich",
|
||||
"rnaseq_fgsea",
|
||||
"rnaseq_candidate_family",
|
||||
"rnaseq_report",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ RNASEQ_STATUS_STAGES = {
|
||||
"rnaseq_fgsea",
|
||||
"rnaseq_wgcna",
|
||||
"rnaseq_candidate_family",
|
||||
"rnaseq_report",
|
||||
"annotation",
|
||||
"annotation_go",
|
||||
"annotation_kegg",
|
||||
|
||||
@@ -1517,6 +1517,19 @@ class StageToolRegistry:
|
||||
"timeout": 7200,
|
||||
"conda_env": "rnaseq"
|
||||
},
|
||||
# 24. Formal RNA-seq Result Report
|
||||
{
|
||||
"id": "rnaseq_report",
|
||||
"name": "RNA-seq Result Report",
|
||||
"script": "run_rnaseq_report.py",
|
||||
"category": "rnaseq",
|
||||
"description": "Generate a scoped formal Markdown report from successful RNA-seq manifests",
|
||||
"required_inputs": [],
|
||||
"outputs": ["rnaseq_report", "rnaseq_report_evidence", "rnaseq_report_model"],
|
||||
"parameters": {"cpu": 4},
|
||||
"timeout": 1800,
|
||||
"conda_env": "test4py311"
|
||||
},
|
||||
# Combo Plot - 组合图
|
||||
{
|
||||
"id": "combo",
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
"""Lightweight, provider-free RNA-seq manifest evidence snapshot.
|
||||
|
||||
The live interpretation view and the formal report share this module so scope
|
||||
selection and manifest provenance have one implementation. It intentionally
|
||||
does not import the MAS agent package.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
|
||||
REPORT_STAGE = "rnaseq_report"
|
||||
SUCCESS_STATUSES = {"success", "partial_success"}
|
||||
GLOBAL_LEGACY_STAGES = {
|
||||
"rnaseq_qc",
|
||||
"rnaseq_align",
|
||||
"rnaseq_quant",
|
||||
"annotation",
|
||||
"annotation_go",
|
||||
"annotation_kegg",
|
||||
}
|
||||
RNASEQ_STAGE_NAMES = {
|
||||
"rnaseq_qc": "RNA-seq质控",
|
||||
"rnaseq_align": "RNA-seq比对",
|
||||
"rnaseq_quant": "RNA-seq定量",
|
||||
"rnaseq_sample_qc": "RNA-seq样本质量评估",
|
||||
"rnaseq_de": "RNA-seq差异表达",
|
||||
"annotation": "背景功能注释",
|
||||
"annotation_go": "GO背景自动注释",
|
||||
"annotation_kegg": "KEGG/KO背景自动注释",
|
||||
"rnaseq_go_enrich": "RNA-seq GO富集分析",
|
||||
"rnaseq_kegg_enrich": "RNA-seq KEGG富集分析",
|
||||
"rnaseq_enrich": "RNA-seq富集分析(兼容入口)",
|
||||
"rnaseq_fgsea": "RNA-seq fgsea排序富集",
|
||||
"rnaseq_wgcna": "RNA-seq WGCNA共表达分析",
|
||||
"rnaseq_candidate_family": "RNA-seq候选家族筛选",
|
||||
"rnaseq_family_score": "RNA-seq候选家族评分",
|
||||
}
|
||||
RNASEQ_STAGE_ORDER = list(RNASEQ_STAGE_NAMES)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RNASeqManifestSelection:
|
||||
rnaseq_scope_id: str
|
||||
discovered_scope_ids: tuple[str, ...]
|
||||
manifests: dict[str, dict[str, Any]]
|
||||
|
||||
|
||||
def read_json(path: Path) -> dict[str, Any]:
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
return data if isinstance(data, dict) else {}
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return {}
|
||||
|
||||
|
||||
def manifest_identity_value(manifest: dict[str, Any], key: str) -> str:
|
||||
context = manifest.get("stage_context") or manifest.get("stageContext") or {}
|
||||
params = manifest.get("parameters") or {}
|
||||
return str(manifest.get(key) or params.get(key) or context.get(key) or "").strip()
|
||||
|
||||
|
||||
def is_rnaseq_evidence_stage(stage: str) -> bool:
|
||||
stage = str(stage or "").strip()
|
||||
return stage != REPORT_STAGE and (
|
||||
stage.startswith("rnaseq_") or stage in {"annotation", "annotation_go", "annotation_kegg"}
|
||||
)
|
||||
|
||||
|
||||
def stage_sort_key(stage: str) -> tuple[int, str]:
|
||||
try:
|
||||
return RNASEQ_STAGE_ORDER.index(stage), stage
|
||||
except ValueError:
|
||||
return len(RNASEQ_STAGE_ORDER), stage
|
||||
|
||||
|
||||
def _manifest_score(path: Path, manifest: dict[str, Any]) -> tuple[int, int, int, float]:
|
||||
status = str(manifest.get("status") or "")
|
||||
status_rank = 2 if status == "success" else 1 if status == "partial_success" else 0
|
||||
metrics = manifest.get("metrics") if isinstance(manifest.get("metrics"), dict) else {}
|
||||
artifacts = manifest.get("artifacts") if isinstance(manifest.get("artifacts"), dict) else {}
|
||||
try:
|
||||
mtime = path.stat().st_mtime
|
||||
except OSError:
|
||||
mtime = 0.0
|
||||
return status_rank, len(metrics), len(artifacts), mtime
|
||||
|
||||
|
||||
def select_rnaseq_manifests(
|
||||
final_dir: Path,
|
||||
prefix: str,
|
||||
*,
|
||||
rnaseq_scope_id: str = "",
|
||||
) -> RNASeqManifestSelection:
|
||||
"""Select one non-mixing manifest per RNA-seq stage for a prefix/scope."""
|
||||
final_dir = Path(final_dir)
|
||||
pattern = f"{prefix}.*.manifest.json" if prefix else "*.manifest.json"
|
||||
candidates: list[tuple[Path, dict[str, Any]]] = []
|
||||
latest_by_scope: dict[str, float] = {}
|
||||
for path in sorted(final_dir.glob(pattern)) if final_dir.exists() else []:
|
||||
manifest = read_json(path)
|
||||
stage = str(manifest.get("stage") or "")
|
||||
if not is_rnaseq_evidence_stage(stage):
|
||||
continue
|
||||
manifest_prefix = str(manifest.get("prefix") or "").strip()
|
||||
if prefix and manifest_prefix and manifest_prefix != prefix:
|
||||
continue
|
||||
candidates.append((path, manifest))
|
||||
scope = manifest_identity_value(manifest, "rnaseq_scope_id")
|
||||
if scope:
|
||||
try:
|
||||
latest_by_scope[scope] = max(latest_by_scope.get(scope, 0.0), path.stat().st_mtime)
|
||||
except OSError:
|
||||
latest_by_scope.setdefault(scope, 0.0)
|
||||
|
||||
discovered = sorted(latest_by_scope, key=lambda item: (-latest_by_scope[item], item))
|
||||
resolved_scope = str(rnaseq_scope_id or "").strip() or (discovered[0] if discovered else "")
|
||||
selected_paths: dict[str, Path] = {}
|
||||
selected: dict[str, dict[str, Any]] = {}
|
||||
for path, manifest in candidates:
|
||||
stage = str(manifest.get("stage") or "")
|
||||
scope = manifest_identity_value(manifest, "rnaseq_scope_id")
|
||||
if resolved_scope and scope and scope != resolved_scope:
|
||||
continue
|
||||
if resolved_scope and not scope and stage not in GLOBAL_LEGACY_STAGES:
|
||||
continue
|
||||
existing = selected.get(stage)
|
||||
if existing is not None and _manifest_score(selected_paths[stage], existing) >= _manifest_score(path, manifest):
|
||||
continue
|
||||
manifest = dict(manifest)
|
||||
manifest["_manifest_path"] = str(path)
|
||||
selected[stage] = manifest
|
||||
selected_paths[stage] = path
|
||||
return RNASeqManifestSelection(resolved_scope, tuple(discovered), selected)
|
||||
|
||||
|
||||
def _compact_value(value: Any, limit: int = 240) -> str:
|
||||
if isinstance(value, (dict, list)):
|
||||
text = json.dumps(value, ensure_ascii=False, sort_keys=True)
|
||||
else:
|
||||
text = str(value)
|
||||
return text if len(text) <= limit else text[: limit - 1] + "…"
|
||||
|
||||
|
||||
def resolve_artifact_path(
|
||||
final_dir: Path,
|
||||
item: dict[str, Any],
|
||||
*,
|
||||
require_exists: bool = False,
|
||||
) -> Path | None:
|
||||
"""Resolve artifacts after a workspace has been moved or mounted elsewhere."""
|
||||
final_dir = Path(final_dir)
|
||||
workspace = final_dir.parent
|
||||
candidates: list[Path] = []
|
||||
for raw in (item.get("full_path"), item.get("path")):
|
||||
if not raw:
|
||||
continue
|
||||
path = Path(str(raw))
|
||||
candidates.append(path if path.is_absolute() else workspace / path)
|
||||
if path.is_absolute():
|
||||
candidates.append(final_dir / path.name)
|
||||
|
||||
unique: list[Path] = []
|
||||
seen: set[str] = set()
|
||||
for path in candidates:
|
||||
identity = str(path)
|
||||
if identity not in seen:
|
||||
seen.add(identity)
|
||||
unique.append(path)
|
||||
for path in unique:
|
||||
if path.exists():
|
||||
return path
|
||||
if require_exists:
|
||||
return None
|
||||
return unique[0] if unique else None
|
||||
|
||||
|
||||
def build_rnaseq_evidence_snapshot(
|
||||
final_dir: Path,
|
||||
prefix: str,
|
||||
*,
|
||||
rnaseq_scope_id: str = "",
|
||||
include_stages: Iterable[str] | None = None,
|
||||
selection: RNASeqManifestSelection | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Build a compact evidence object without invoking an LLM or agent cache."""
|
||||
selection = selection or select_rnaseq_manifests(
|
||||
final_dir, prefix, rnaseq_scope_id=rnaseq_scope_id
|
||||
)
|
||||
allowed = {str(stage) for stage in include_stages or [] if str(stage).strip()}
|
||||
manifests = {
|
||||
stage: manifest
|
||||
for stage, manifest in selection.manifests.items()
|
||||
if not allowed or stage in allowed
|
||||
}
|
||||
stages = sorted(manifests, key=stage_sort_key)
|
||||
stage_flow: list[dict[str, Any]] = []
|
||||
findings: list[dict[str, Any]] = []
|
||||
reviews: list[dict[str, Any]] = []
|
||||
cards: list[dict[str, Any]] = []
|
||||
risks: list[dict[str, Any]] = []
|
||||
card_index = 0
|
||||
for stage in stages:
|
||||
manifest = manifests[stage]
|
||||
status = str(manifest.get("status") or "unknown")
|
||||
metrics = manifest.get("metrics") if isinstance(manifest.get("metrics"), dict) else {}
|
||||
parameters = manifest.get("parameters") if isinstance(manifest.get("parameters"), dict) else {}
|
||||
artifacts = manifest.get("artifacts") if isinstance(manifest.get("artifacts"), dict) else {}
|
||||
metric_items = list(sorted(metrics.items()))[:12]
|
||||
metric_summary = ", ".join(f"{key}={_compact_value(value, 80)}" for key, value in metric_items[:5])
|
||||
key_result = metric_summary or f"登记产物 {len(artifacts)} 个"
|
||||
stage_flow.append(
|
||||
{
|
||||
"id": stage,
|
||||
"name": RNASEQ_STAGE_NAMES.get(stage, stage),
|
||||
"status": status,
|
||||
"key_result": key_result,
|
||||
}
|
||||
)
|
||||
findings.append(
|
||||
{
|
||||
"stage": stage,
|
||||
"title": RNASEQ_STAGE_NAMES.get(stage, stage),
|
||||
"description": f"manifest 状态为 {status};{key_result}。",
|
||||
}
|
||||
)
|
||||
limitations: list[str] = []
|
||||
if status == "partial_success":
|
||||
limitations.append("该阶段为 partial_success,结果可用但不完整。")
|
||||
elif status != "success":
|
||||
limitations.append("该阶段未成功,不纳入正式结论。")
|
||||
missing_artifacts: list[str] = []
|
||||
for name, item in sorted(artifacts.items()):
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
path = resolve_artifact_path(final_dir, item)
|
||||
exists = bool(path and path.exists())
|
||||
card_index += 1
|
||||
cards.append(
|
||||
{
|
||||
"id": f"RNA-E{card_index:04d}",
|
||||
"stage": stage,
|
||||
"claim": f"产物 {name}",
|
||||
"evidence": f"path={path or ''}, exists={exists}",
|
||||
"source_type": "raw_file" if exists else "manifest",
|
||||
}
|
||||
)
|
||||
if not exists:
|
||||
missing_artifacts.append(str(name))
|
||||
for key, value in metric_items:
|
||||
card_index += 1
|
||||
cards.append(
|
||||
{
|
||||
"id": f"RNA-E{card_index:04d}",
|
||||
"stage": stage,
|
||||
"claim": f"指标 {key}",
|
||||
"evidence": _compact_value(value),
|
||||
"source_type": "manifest",
|
||||
}
|
||||
)
|
||||
safe_parameter_items = [
|
||||
(key, value)
|
||||
for key, value in sorted(parameters.items())
|
||||
if not any(secret in str(key).lower() for secret in ("api_key", "token", "secret", "password"))
|
||||
][:10]
|
||||
for key, value in safe_parameter_items:
|
||||
card_index += 1
|
||||
cards.append(
|
||||
{
|
||||
"id": f"RNA-E{card_index:04d}",
|
||||
"stage": stage,
|
||||
"claim": f"参数 {key}",
|
||||
"evidence": _compact_value(value),
|
||||
"source_type": "manifest",
|
||||
}
|
||||
)
|
||||
if missing_artifacts:
|
||||
shown = missing_artifacts[:12]
|
||||
remainder = len(missing_artifacts) - len(shown)
|
||||
suffix = f"(另有 {remainder} 个,详见 evidence JSON)" if remainder else ""
|
||||
message = f"manifest 登记但文件不存在的产物:{', '.join(shown)}{suffix}"
|
||||
limitations.append(message)
|
||||
risks.append({"stage": stage, "severity": "high", "message": message})
|
||||
if status not in SUCCESS_STATUSES:
|
||||
risks.append({"stage": stage, "severity": "high", "message": f"阶段状态为 {status},不得作为成功证据。"})
|
||||
reviews.append({"stage": stage, "stage_name": RNASEQ_STAGE_NAMES.get(stage, stage), "limitations": limitations})
|
||||
|
||||
successful = [stage for stage in stages if str(manifests[stage].get("status") or "") in SUCCESS_STATUSES]
|
||||
return {
|
||||
"schema": "rnaseq_manifest_evidence_v1",
|
||||
"metadata": {
|
||||
"prefix": prefix,
|
||||
"rnaseq_scope_id": selection.rnaseq_scope_id,
|
||||
"discovered_scope_ids": list(selection.discovered_scope_ids),
|
||||
"included_stages": stages,
|
||||
"successful_stages": successful,
|
||||
"interpretation_boundary": "仅陈述当前 prefix 与 RNA-seq scope 下 manifest/产物支持的事实;富集、相关性和候选排序不得写成调控因果。",
|
||||
},
|
||||
"stage_flow": {"visualization": " -> ".join(stages), "stages": stage_flow},
|
||||
"key_findings": findings,
|
||||
"stage_reviews": reviews,
|
||||
"evidence_cards": cards,
|
||||
"risk_flags": risks,
|
||||
"major_claims": [],
|
||||
"biological_story": (
|
||||
f"当前报告汇总了 {len(successful)} 个成功或部分成功的 RNA-seq 阶段。"
|
||||
"跨阶段生物学解释必须以各阶段数值、图表和风险标记为边界。"
|
||||
),
|
||||
"next_steps": ["补齐缺失或失败阶段后重新生成报告,以刷新证据快照。"],
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import mimetypes
|
||||
import os
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
from urllib.parse import unquote
|
||||
@@ -79,8 +79,21 @@ class RNASeqReportResult:
|
||||
reused: bool = False
|
||||
|
||||
|
||||
BEIJING_TIMEZONE = timezone(timedelta(hours=8))
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return datetime.now().astimezone().isoformat(timespec="seconds")
|
||||
return datetime.now(BEIJING_TIMEZONE).isoformat(timespec="seconds")
|
||||
|
||||
|
||||
def _display_generated_at(value: str) -> str:
|
||||
try:
|
||||
parsed = datetime.fromisoformat(value)
|
||||
if parsed.tzinfo is None:
|
||||
parsed = parsed.replace(tzinfo=BEIJING_TIMEZONE)
|
||||
return parsed.astimezone(BEIJING_TIMEZONE).strftime("%Y-%m-%d %H:%M:%S")
|
||||
except (TypeError, ValueError):
|
||||
return str(value or "").replace("T", " ")
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
@@ -1224,7 +1237,7 @@ def _render_deterministic_markdown(
|
||||
lines = [
|
||||
f"# {prefix} RNA-seq 结果解读报告",
|
||||
"",
|
||||
f"生成时间:{generated_at} ",
|
||||
f"生成时间(北京时间):{_display_generated_at(generated_at)} ",
|
||||
f"任务编号:`{workspace.name}`",
|
||||
"",
|
||||
"**任务结果路径:** ",
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# RNA-seq Result Report Contract v1
|
||||
|
||||
## Stage
|
||||
|
||||
- Stage id: `rnaseq_report`
|
||||
- Script: `apps/cli/run_rnaseq_report.py`
|
||||
- Scope: one exact `prefix + rnaseq_scope_id` evidence snapshot
|
||||
- Purpose: materialize a formal, reproducible RNA-seq Markdown result report without depending on frontend session state.
|
||||
|
||||
## Inputs
|
||||
|
||||
- At least one RNA-seq/annotation manifest with status `success` or `partial_success` under `final/`.
|
||||
- All selected evidence must match the requested prefix and RNA-seq scope. Legacy unscoped QC/alignment/quantification/annotation manifests may be treated as experiment-global evidence.
|
||||
- `expected_stages` is optional and records the stages selected for the current batch. It controls coverage only; it must not become a static execution dependency.
|
||||
- An existing configured `LLMManager` provider is optional. No provider or model name is required by this stage.
|
||||
|
||||
If no successful RNA-seq evidence exists, the stage must return `暂无可生成的 RNA-seq 结果` and must not create an empty report.
|
||||
|
||||
## Required Outputs
|
||||
|
||||
- `final/{prefix}.rnaseq_report.md`
|
||||
- `final/{prefix}.rnaseq_report.evidence.json`
|
||||
- `final/{prefix}.rnaseq_report.model.json`
|
||||
- `final/{prefix}.rnaseq_report.manifest.json`
|
||||
|
||||
The manifest stage status is `success` whenever these outputs are generated. Content completeness is recorded separately as `metrics.coverage_status = complete|partial`.
|
||||
|
||||
## Generation Semantics
|
||||
|
||||
- `generate`: always rebuild from the current manifest snapshot.
|
||||
- `open`: reuse a fresh report; regenerate first when the source/request fingerprint changed.
|
||||
- The deterministic Markdown body is always complete enough to read without an API.
|
||||
- Optional model output is a labeled language-enhancement section only. Model failure must retain the deterministic report and be recorded in `.model.json`.
|
||||
- `rnaseq_report` manifests are excluded from their own evidence scan.
|
||||
|
||||
## Boundary With Deep Interpretation
|
||||
|
||||
Both features may call the pure `gfa_core.rnaseq_evidence` snapshot builder. The formal report must not call `/api/agent/interpret`, read its session cache, or replace the frontend deep-interpretation view.
|
||||
|
||||
## Validation
|
||||
|
||||
- Every manifest artifact path exists and its digest is recorded.
|
||||
- Evidence contains only the selected prefix/scope.
|
||||
- `coverage_status=complete` only when every expected stage has status `success`.
|
||||
- Missing, failed, or `partial_success` expected stages yield `coverage_status=partial` while the report manifest remains `success`.
|
||||
@@ -33,10 +33,6 @@
|
||||
|
||||
各阶段依次写明分析对象、源结果中的直接观察、谨慎解释、限制,并仅在有助复核时列出少量相对路径。
|
||||
|
||||
### 2.2 语言边界
|
||||
|
||||
预测关系统一表述为线索;没有逐成员表或独立证据时,不指定候选基因,不声称功能改变或调控关系已经证实。
|
||||
|
||||
<!-- GFA_SECTION common.completeness -->
|
||||
## 三、结果总览与完整性检查
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _block(source: str, start: str, end: str) -> str:
|
||||
begin = source.index(start)
|
||||
finish = source.index(end, begin)
|
||||
return source[begin:finish]
|
||||
|
||||
|
||||
def test_multilingual_frontend_keeps_critical_ppi_kaks_state_chain_in_sync() -> None:
|
||||
canonical = (ROOT / "apps/web/vue3/index.html").read_text(encoding="utf-8")
|
||||
multilingual = (ROOT / "apps/web/vue3/index_ml.html").read_text(encoding="utf-8")
|
||||
boundaries = (
|
||||
(
|
||||
"const resumePpiKaksPausedStages = async",
|
||||
"const downloadRecommendedPpiReferenceBundle = async",
|
||||
),
|
||||
(
|
||||
"const monitorPpiReferenceBundleDownload =",
|
||||
"const ppiHasNearReferenceBeforeArabidopsis =",
|
||||
),
|
||||
(
|
||||
"const ensurePpiKaksDependencyOwner = async",
|
||||
"const submitExecuteRequestWithAutoAnnotation = async",
|
||||
),
|
||||
(
|
||||
"const kaksReferenceChoiceParams =",
|
||||
"const confirmedStageParamsForExecution =",
|
||||
),
|
||||
)
|
||||
|
||||
for start, end in boundaries:
|
||||
assert _block(multilingual, start, end) == _block(canonical, start, end)
|
||||
Reference in New Issue
Block a user