lobster-transcriptomics 1.2.0__tar.gz → 1.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {lobster_transcriptomics-1.2.0/lobster_transcriptomics.egg-info → lobster_transcriptomics-1.2.2}/PKG-INFO +1 -1
  2. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/agents/transcriptomics/annotation_expert.py +6 -0
  3. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/agents/transcriptomics/de_analysis_expert.py +87 -30
  4. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/agents/transcriptomics/prompts.py +55 -2
  5. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/agents/transcriptomics/shared_tools.py +152 -38
  6. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/agents/transcriptomics/transcriptomics_expert.py +132 -27
  7. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/services/analysis/enhanced_singlecell_service.py +186 -100
  8. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/services/quality/preprocessing_service.py +54 -17
  9. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/services/quality/quality_service.py +42 -0
  10. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2/lobster_transcriptomics.egg-info}/PKG-INFO +1 -1
  11. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/pyproject.toml +1 -1
  12. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/LICENSE +0 -0
  13. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/README.md +0 -0
  14. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/agents/transcriptomics/__init__.py +0 -0
  15. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/agents/transcriptomics/config.py +0 -0
  16. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/agents/transcriptomics/py.typed +0 -0
  17. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/agents/transcriptomics/state.py +0 -0
  18. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/figure_template_providers/sc.py +0 -0
  19. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/services/analysis/bulk_preprocessing_service.py +0 -0
  20. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/services/analysis/differential_formula_service.py +0 -0
  21. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/services/analysis/pseudobulk_service.py +0 -0
  22. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/services/metadata/manual_annotation_service.py +0 -0
  23. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/services/templates/annotation_templates.py +0 -0
  24. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/services/visualization/bulk_visualization_service.py +0 -0
  25. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster_transcriptomics.egg-info/SOURCES.txt +0 -0
  26. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster_transcriptomics.egg-info/dependency_links.txt +0 -0
  27. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster_transcriptomics.egg-info/entry_points.txt +0 -0
  28. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster_transcriptomics.egg-info/requires.txt +0 -0
  29. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster_transcriptomics.egg-info/top_level.txt +0 -0
  30. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lobster-transcriptomics
3
- Version: 1.2.0
3
+ Version: 1.2.2
4
4
  Summary: Transcriptomics agents for Lobster AI - single-cell and bulk RNA-seq analysis
5
5
  Author-email: Omics-OS <info@omics-os.com>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -54,6 +54,7 @@ from lobster.config.inference_context import (
54
54
  from lobster.config.llm_factory import create_llm
55
55
  from lobster.config.settings import get_settings
56
56
  from lobster.core.component_registry import component_registry
57
+ from lobster.core.export_scope import WORKSPACE_EXPORT_MESSAGE, outside_workspace
57
58
  from lobster.core.figures.export_guard import MANAGED_EXPORT_MESSAGE, export_refused
58
59
  from lobster.core.runtime.data_manager import DataManagerV2
59
60
  from lobster.services.analysis.enhanced_singlecell_service import (
@@ -1067,12 +1068,17 @@ def annotation_expert(
1067
1068
  output_path = Path(output_filename)
1068
1069
  if not output_path.is_absolute():
1069
1070
  output_path = data_manager.exports_dir / output_path
1071
+ # The destination stays inside the workspace (or a listed export root).
1072
+ if outside_workspace(data_manager.workspace_path, output_path):
1073
+ return WORKSPACE_EXPORT_MESSAGE
1070
1074
  # Figures are saved only through the trusted save step: check the
1071
1075
  # caller's (unresolved) destination before anything is created.
1072
1076
  if export_refused(data_manager.workspace_path, output_path):
1073
1077
  return MANAGED_EXPORT_MESSAGE
1074
1078
  output_path.parent.mkdir(parents=True, exist_ok=True)
1075
1079
  output_filename = str(output_path.resolve())
1080
+ if outside_workspace(data_manager.workspace_path, output_filename):
1081
+ return WORKSPACE_EXPORT_MESSAGE
1076
1082
  if export_refused(
1077
1083
  data_manager.workspace_path, output_path, output_filename
1078
1084
  ):
@@ -53,6 +53,15 @@ from lobster.core import (
53
53
  InsufficientCellsError,
54
54
  PseudobulkError,
55
55
  )
56
+ from lobster.core.export_scope import (
57
+ WORKSPACE_EXPORT_MESSAGE,
58
+ InvalidModalityNameError,
59
+ WorkspaceExportError,
60
+ assert_inside_workspace,
61
+ check_modality_name,
62
+ outside_workspace,
63
+ plain_name_part,
64
+ )
56
65
  from lobster.core.figures.errors import ManagedPathError
57
66
  from lobster.core.figures.export_dirs import assert_dirs_allowed
58
67
  from lobster.core.figures.export_guard import (
@@ -89,6 +98,15 @@ _BULK_SERVICE_SETUP = (
89
98
  )
90
99
 
91
100
 
101
+ def _de_result_name(modality_name: str, group1, group2) -> str:
102
+ """The DE result modality name: group labels are data values, so each is encoded
103
+ as part of one plain name (two labels never collide); refused (fixed reason)
104
+ before any analysis if the name is still not plain, for example too long."""
105
+ return check_modality_name(
106
+ f"{modality_name}_de_{plain_name_part(group1)}_vs_{plain_name_part(group2)}"
107
+ )
108
+
109
+
92
110
  def _is_pseudobulk(adata) -> bool:
93
111
  """True when the modality carries a pseudobulk or formula design (``uns`` only)."""
94
112
  return (
@@ -1091,7 +1109,9 @@ def de_analysis_expert(
1091
1109
  status = (
1092
1110
  "OK"
1093
1111
  if count >= 4
1094
- else "LOW POWER" if count >= 3 else "INSUFFICIENT"
1112
+ else "LOW POWER"
1113
+ if count >= 3
1114
+ else "INSUFFICIENT"
1095
1115
  )
1096
1116
  response += f"\n- {group}: {count} replicates ({status})"
1097
1117
  response += _replicate_summary_text(replicate_validation)
@@ -1174,21 +1194,26 @@ def de_analysis_expert(
1174
1194
  # Get the modality
1175
1195
  adata = data_manager.get_modality(modality_name)
1176
1196
 
1197
+ # The result name carries the group labels (data values, which may hold
1198
+ # '/'): encoded into one plain name, checked before any analysis.
1199
+ de_modality_name = None
1200
+ if not (_is_pseudobulk(adata) and method == "deseq2"):
1201
+ de_modality_name = _de_result_name(modality_name, group1, group2)
1202
+
1177
1203
  # Every save target of the branch that will run is checked before the
1178
1204
  # data changes or any analysis runs.
1179
1205
  if save_result:
1180
1206
  if _is_pseudobulk(adata) and method == "deseq2":
1181
- assert_export_allowed(
1182
- data_manager.workspace_path,
1183
- data_manager.exports_dir / f"{modality_name}_de_results.csv",
1207
+ results_csv = (
1208
+ data_manager.exports_dir / f"{modality_name}_de_results.csv"
1184
1209
  )
1210
+ assert_inside_workspace(data_manager.workspace_path, results_csv)
1211
+ assert_export_allowed(data_manager.workspace_path, results_csv)
1185
1212
  data_manager._assert_save_allowed(
1186
1213
  f"{modality_name}_with_de_results.h5ad"
1187
1214
  )
1188
1215
  else:
1189
- data_manager._assert_save_allowed(
1190
- f"{modality_name}_de_{group1}_vs_{group2}.h5ad"
1191
- )
1216
+ data_manager._assert_save_allowed(f"{de_modality_name}.h5ad")
1192
1217
 
1193
1218
  logger.info(
1194
1219
  f"Running DE analysis on '{modality_name}': "
@@ -1404,8 +1429,7 @@ def de_analysis_expert(
1404
1429
  de_stats["de_results_key"],
1405
1430
  "adata_de" if internal_method == "deseq2_like" else "adata",
1406
1431
  )
1407
- # Save as new modality
1408
- de_modality_name = f"{modality_name}_de_{group1}_vs_{group2}"
1432
+ # Save as new modality (name derived and checked before the analysis)
1409
1433
  data_manager.store_modality(
1410
1434
  name=de_modality_name,
1411
1435
  adata=adata_de,
@@ -1414,7 +1438,7 @@ def de_analysis_expert(
1414
1438
  )
1415
1439
 
1416
1440
  if save_result:
1417
- save_path = f"{modality_name}_de_{group1}_vs_{group2}.h5ad"
1441
+ save_path = f"{de_modality_name}.h5ad"
1418
1442
  data_manager.save_modality(de_modality_name, save_path)
1419
1443
 
1420
1444
  data_manager.log_tool_usage(
@@ -1437,13 +1461,13 @@ def de_analysis_expert(
1437
1461
 
1438
1462
  **Analysis Results:**
1439
1463
  - Comparison: {group1} ({group1_count} samples) vs {group2} ({group2_count} samples)
1440
- - Method: {de_stats['method']}
1441
- - Genes tested: {de_stats['n_genes_tested']:,}
1442
- - Significant genes (padj < {alpha}): {de_stats['n_significant_genes']:,}
1464
+ - Method: {de_stats["method"]}
1465
+ - Genes tested: {de_stats["n_genes_tested"]:,}
1466
+ - Significant genes (padj < {alpha}): {de_stats["n_significant_genes"]:,}
1443
1467
 
1444
1468
  **Differential Expression Summary:**
1445
- - Upregulated in {group2}: {de_stats['n_upregulated']} genes
1446
- - Downregulated in {group2}: {de_stats['n_downregulated']} genes
1469
+ - Upregulated in {group2}: {de_stats["n_upregulated"]} genes
1470
+ - Downregulated in {group2}: {de_stats["n_downregulated"]} genes
1447
1471
 
1448
1472
  **Top Upregulated Genes:**
1449
1473
  {chr(10).join([f"- {gene}" for gene in de_stats["top_upregulated"][:5]])}
@@ -1471,6 +1495,12 @@ def de_analysis_expert(
1471
1495
  analysis_results["details"]["differential_expression"] = response
1472
1496
  return response
1473
1497
 
1498
+ except WorkspaceExportError:
1499
+ # A save target is outside the workspace: nothing was saved.
1500
+ return WORKSPACE_EXPORT_MESSAGE
1501
+ except InvalidModalityNameError as e:
1502
+ # The derived result name is not one plain name (fixed reason).
1503
+ return f"Error: {e}"
1474
1504
  except ManagedPathError:
1475
1505
  # A save target is inside the managed figure folder: nothing was saved.
1476
1506
  return MANAGED_EXPORT_MESSAGE
@@ -1897,7 +1927,9 @@ def de_analysis_expert(
1897
1927
  status = (
1898
1928
  "OK"
1899
1929
  if count >= 4
1900
- else "LOW POWER" if count >= 3 else "INSUFFICIENT"
1930
+ else "LOW POWER"
1931
+ if count >= 3
1932
+ else "INSUFFICIENT"
1901
1933
  )
1902
1934
  response += f"- {group}: {count} replicates ({status})\n"
1903
1935
  response += _replicate_summary_text(replicate_validation)
@@ -1989,11 +2021,12 @@ def de_analysis_expert(
1989
2021
 
1990
2022
  # Both save targets are checked before any analysis or store.
1991
2023
  if save_results:
1992
- assert_export_allowed(
1993
- data_manager.workspace_path,
2024
+ formula_csv = (
1994
2025
  data_manager.exports_dir
1995
- / f"{pseudobulk_modality}_formula_de_results.csv",
2026
+ / f"{pseudobulk_modality}_formula_de_results.csv"
1996
2027
  )
2028
+ assert_inside_workspace(data_manager.workspace_path, formula_csv)
2029
+ assert_export_allowed(data_manager.workspace_path, formula_csv)
1997
2030
  data_manager._assert_save_allowed(
1998
2031
  f"{pseudobulk_modality}_with_formula_results.h5ad"
1999
2032
  )
@@ -2192,6 +2225,9 @@ def de_analysis_expert(
2192
2225
 
2193
2226
  return response
2194
2227
 
2228
+ except WorkspaceExportError:
2229
+ # A save target is outside the workspace: nothing was saved.
2230
+ return WORKSPACE_EXPORT_MESSAGE
2195
2231
  except ManagedPathError:
2196
2232
  # A save target is inside the managed figure folder: nothing was saved.
2197
2233
  return MANAGED_EXPORT_MESSAGE
@@ -3034,6 +3070,9 @@ def de_analysis_expert(
3034
3070
  output_path_obj = Path(output_path)
3035
3071
  if not output_path_obj.is_absolute():
3036
3072
  output_path_obj = data_manager.exports_dir / output_path_obj
3073
+ # The destination stays inside the workspace (or a listed export root).
3074
+ if outside_workspace(data_manager.workspace_path, output_path_obj):
3075
+ return WORKSPACE_EXPORT_MESSAGE
3037
3076
  # Figures are saved only through the trusted save step: check the
3038
3077
  # caller's (unresolved) destination, and every folder its recursive
3039
3078
  # creation can make, before anything is created.
@@ -3045,6 +3084,8 @@ def de_analysis_expert(
3045
3084
  return MANAGED_EXPORT_MESSAGE
3046
3085
  output_path_obj.parent.mkdir(parents=True, exist_ok=True)
3047
3086
  output_path = str(output_path_obj.resolve())
3087
+ if outside_workspace(data_manager.workspace_path, output_path):
3088
+ return WORKSPACE_EXPORT_MESSAGE
3048
3089
  if export_refused(
3049
3090
  data_manager.workspace_path, output_path_obj, output_path
3050
3091
  ):
@@ -3080,7 +3121,7 @@ def de_analysis_expert(
3080
3121
  "results_df = results_df.sort_values({{ sort_by | string | repr }})\n"
3081
3122
  "results_df.to_{{ output_format }}({{ output_path | string | repr }}, index=True)"
3082
3123
  ),
3083
- imports=["pandas"],
3124
+ imports=["import pandas as pd"],
3084
3125
  )
3085
3126
 
3086
3127
  data_manager.log_tool_usage(
@@ -3107,8 +3148,8 @@ def de_analysis_expert(
3107
3148
  - Sorted by: {sort_by}
3108
3149
  - Output: {output_path}
3109
3150
 
3110
- **Columns:** {', '.join(results_df.columns.tolist()[:8])}{'...' if len(results_df.columns) > 8 else ''}
3111
- **Filtered only:** {'Yes' if filtered_only else 'No'}"""
3151
+ **Columns:** {", ".join(results_df.columns.tolist()[:8])}{"..." if len(results_df.columns) > 8 else ""}
3152
+ **Filtered only:** {"Yes" if filtered_only else "No"}"""
3112
3153
 
3113
3154
  return response
3114
3155
 
@@ -3189,6 +3230,9 @@ def de_analysis_expert(
3189
3230
  if group2 not in available_groups_str:
3190
3231
  return f"Group '{group2}' not found in '{group_key}'. Available: {available_groups}"
3191
3232
 
3233
+ # The result name carries the group labels: encoded and checked first.
3234
+ de_modality_name = _de_result_name(modality_name, group1, group2)
3235
+
3192
3236
  # Ensure string type matching
3193
3237
  if group1 not in available_groups:
3194
3238
  adata.obs[group_key] = adata.obs[group_key].astype(str)
@@ -3223,8 +3267,7 @@ def de_analysis_expert(
3223
3267
  if refusal:
3224
3268
  return refusal
3225
3269
  adata_de.uns["de_current_results_key"] = de_stats["de_results_key"]
3226
- # Store result modality
3227
- de_modality_name = f"{modality_name}_de_{group1}_vs_{group2}"
3270
+ # Store result modality (name derived and checked before the analysis)
3228
3271
  data_manager.store_modality(
3229
3272
  name=de_modality_name,
3230
3273
  adata=adata_de,
@@ -3292,7 +3335,7 @@ def de_analysis_expert(
3292
3335
  **Method:** DESeq2-like bulk differential expression
3293
3336
 
3294
3337
  **Results:**
3295
- - Genes tested: {de_stats.get('n_genes_tested', 'N/A'):,}
3338
+ - Genes tested: {de_stats.get("n_genes_tested", "N/A"):,}
3296
3339
  - Significant genes (padj < {alpha}): {n_sig:,}
3297
3340
  - Upregulated in {group2}: {n_up:,}
3298
3341
  - Downregulated in {group2}: {n_down:,}
@@ -3320,6 +3363,9 @@ def de_analysis_expert(
3320
3363
 
3321
3364
  return response
3322
3365
 
3366
+ except InvalidModalityNameError as e:
3367
+ # The derived result name is not one plain name (fixed reason).
3368
+ return f"Error: {e}"
3323
3369
  except (BulkRNASeqError, ModalityNotFoundError) as e:
3324
3370
  logger.error(f"Error in bulk DE direct: {e}")
3325
3371
  return f"Error in bulk DE analysis: {str(e)}"
@@ -3498,7 +3544,7 @@ def de_analysis_expert(
3498
3544
  - DE results: {de_results_key}
3499
3545
  - Ranking metric: {ranking_metric}
3500
3546
  - Genes ranked: {len(ranked_df):,}
3501
- - Databases: {', '.join(db_list)}
3547
+ - Databases: {", ".join(db_list)}
3502
3548
  - Organism: {organism}
3503
3549
 
3504
3550
  **Results:**
@@ -3688,12 +3734,19 @@ def de_analysis_expert(
3688
3734
  targets = [output_path_obj]
3689
3735
  if output_format == "xlsx":
3690
3736
  targets.append(Path(str(output_path_obj).replace(".xlsx", ".csv")))
3737
+ # Every target stays inside the workspace (or a listed export root).
3738
+ if outside_workspace(data_manager.workspace_path, *targets):
3739
+ return WORKSPACE_EXPORT_MESSAGE
3691
3740
  if export_refused(data_manager.workspace_path, *targets):
3692
3741
  return MANAGED_EXPORT_MESSAGE
3693
3742
  if output_format == "xlsx":
3694
3743
  # ...and the name exactly as the late fallback derives it; a path
3695
3744
  # that cannot be resolved is refused with the same fixed message.
3696
3745
  fallback = _resolved_csv_fallback(output_path_obj)
3746
+ if fallback is not None and outside_workspace(
3747
+ data_manager.workspace_path, fallback
3748
+ ):
3749
+ return WORKSPACE_EXPORT_MESSAGE
3697
3750
  if fallback is None or export_refused(
3698
3751
  data_manager.workspace_path, fallback
3699
3752
  ):
@@ -3705,6 +3758,8 @@ def de_analysis_expert(
3705
3758
  return MANAGED_EXPORT_MESSAGE
3706
3759
  output_path_obj.parent.mkdir(parents=True, exist_ok=True)
3707
3760
  output_path = str(output_path_obj.resolve())
3761
+ if outside_workspace(data_manager.workspace_path, output_path):
3762
+ return WORKSPACE_EXPORT_MESSAGE
3708
3763
  if export_refused(data_manager.workspace_path, *targets, output_path):
3709
3764
  return MANAGED_EXPORT_MESSAGE
3710
3765
 
@@ -3714,6 +3769,8 @@ def de_analysis_expert(
3714
3769
  except ImportError:
3715
3770
  output_format = "csv"
3716
3771
  output_path = output_path.replace(".xlsx", ".csv")
3772
+ if outside_workspace(data_manager.workspace_path, output_path):
3773
+ return WORKSPACE_EXPORT_MESSAGE
3717
3774
  if export_refused(data_manager.workspace_path, output_path):
3718
3775
  return MANAGED_EXPORT_MESSAGE
3719
3776
  results_df.to_csv(output_path, index=True)
@@ -3748,7 +3805,7 @@ def de_analysis_expert(
3748
3805
  "results_df = results_df.sort_values('padj')\n"
3749
3806
  "results_df.to_{{ output_format }}({{ output_path | string | repr }}, index=True)"
3750
3807
  ),
3751
- imports=["pandas"],
3808
+ imports=["import pandas as pd"],
3752
3809
  )
3753
3810
 
3754
3811
  data_manager.log_tool_usage(
@@ -3780,10 +3837,10 @@ def de_analysis_expert(
3780
3837
  - Format: {output_format.upper()}
3781
3838
  - Output: {output_path}
3782
3839
 
3783
- **LFC Shrinkage:** {'Applied' if shrinkage_applied else 'Not applied'}
3784
- {f'- Note: {shrinkage_note}' if shrinkage_note else ''}
3840
+ **LFC Shrinkage:** {"Applied" if shrinkage_applied else "Not applied"}
3841
+ {f"- Note: {shrinkage_note}" if shrinkage_note else ""}
3785
3842
 
3786
- **Publication Columns:** {', '.join(pub_cols)}
3843
+ **Publication Columns:** {", ".join(pub_cols)}
3787
3844
 
3788
3845
  **File ready for publication supplementary tables.**"""
3789
3846
 
@@ -144,8 +144,8 @@ Do NOT describe delegation, do NOT ask permission -- execute the tool call.
144
144
  4. select_variable_features("modality_filtered_normalized") # HVG/deviance selection
145
145
  5. run_pca("modality_..._hvg_selected") # PCA reduction
146
146
  6. integrate_batches("modality_..._pca", batch_key="...") # If multi-sample
147
- 7. compute_neighbors_and_embed("modality_..._integrated") # Neighbor graph + UMAP
148
- 8. cluster_cells("modality_..._embedded", resolution=0.5) # Leiden clustering (or algorithm="louvain" if specified)
147
+ 7. compute_neighbors_and_embed("chosen_prepared_modality", use_rep="actual_obsm_key") # Use returned representation key
148
+ 8. cluster_cells("chosen_prepared_modality", use_rep="actual_obsm_key", batch_correction=False, resolution=0.5) # Reuse; do not rerun upstream
149
149
  9. find_marker_genes("modality_clustered", groupby="leiden") # Cluster markers
150
150
  10. handoff_to_annotation_expert(...) # If annotation requested
151
151
  ```
@@ -153,6 +153,23 @@ Do NOT describe delegation, do NOT ask permission -- execute the tool call.
153
153
  NOTE: cluster_cells can also perform steps 4-9 automatically as a full pipeline.
154
154
  Use composable tools when user asks for specific steps; use cluster_cells for full pipeline.
155
155
 
156
+ **Prepared clustering contract:** If the caller already prepared PCA, explicitly name
157
+ that modality and use_rep='X_pca'; pass requested n_neighbors/n_pcs. Only the first
158
+ n_pcs components are used. Do not repeat normalization, HVG selection, scaling or PCA.
159
+ A modality name suffix is not evidence that an embedding exists or is scientifically
160
+ fresh: inspect actual obsm keys. Missing/incompatible representations or unhonorable
161
+ explicit dimensions require refusal, not a silent full-pipeline fallback or clamp.
162
+ For **HVG-only** prepared data without PCA, call composable run_pca on the named
163
+ selected modality first, then cluster its returned modality with use_rep='X_pca'.
164
+ Disclose that PCA was computed, not reused. Unset clustering dimensions resolve the
165
+ ordinary 15/30; dimension-driven implicit adjustments must be disclosed. Explicit
166
+ values are never demo-capped. Read actual applied-parameter JSON and recorded IR,
167
+ not schema/request values, when reporting what ran. Never fabricate an applied HVG
168
+ feature count/flavor: full-pipeline exact HVG requests are unsupported; use the
169
+ explicit selection/PCA/reuse workflow. Prepared reuse skips unrequested batch
170
+ correction and says why; explicit batch correction AND a named prepared representation
171
+ refuse together. If correction is needed, do it before preparing/naming the representation.
172
+
156
173
  ## Bulk Standard Workflow (6 steps)
157
174
 
158
175
  ```
@@ -210,6 +227,36 @@ Report: data_type, n_genes, n_cells/n_samples, mito_pct, n_filtered, cluster_siz
210
227
  If batch_silhouette > 0.3 or median_lisi < 1.5, flag for re-integration.
211
228
  </Response_Format>
212
229
 
230
+ <Scientific_Reporting_Contract>
231
+ - Report dataset-specific numbers only from tool output for the stated cohort. Never infer
232
+ medians, percentiles, exceedance counts or filtering impact from means/default pass counts.
233
+ - If a requested value is absent, say NOT COMPUTED and offer to compute it; "~" is not evidence.
234
+ - Pass requested thresholds unchanged. Report applied parameters from the tool's actual result,
235
+ not from the user's request, a standard workflow or an earlier modality's configuration.
236
+ - Requested-but-unsupported parameters or extra operations: refuse or explicitly state NOT
237
+ SUPPORTED; never claim they were applied. Exact requests take precedence over default workflows.
238
+ - Report every actually applied filter, including explicit optional-filter disabled states.
239
+ Criterion failures are independent on the original pre-filter cohort: do not sum them.
240
+ Report combined actual removals separately, and distinguish gene counts from observation counts.
241
+ - State the real raw-count storage location returned by the tool. adata.raw is not a counts layer.
242
+ - The supervisor must preserve these numeric/cohort/applied-parameter limitations when reformulating.
243
+ - SC markers: original ranked genes, specificity-filtered candidates and FDR-significant genes
244
+ are different sets. Use the tool's explicit original/filtered/display keys, executed groups
245
+ and aligned counts; never label original rankings as filtered or automatically significant.
246
+ - Disclose actual Scanpy specificity operators/units: positive log2FC > min_fold_change,
247
+ in-group fraction > min_pct, out-group fraction < max_out_pct, compare_abs=False.
248
+ The legacy 1.5 threshold is log2FC, not linear fold-change. No FDR criterion is applied there.
249
+ - Supported adjusted p-values must be interpreted using an explicitly stated criterion.
250
+ Absent statistics are NOT COMPUTED, not zero discoveries; nonfinite values are separate flags.
251
+ Finite extreme abs(log2FC) flags use the tool's diagnostic threshold, without clipping,
252
+ reranking or a significance claim. Near-zero reference expression
253
+ can amplify Scanpy's approximate effect size; an extreme value alone proves neither a
254
+ statistical effect nor a computation error.
255
+ - Cell-level cluster-versus-rest markers are exploratory. Single-donor cells are not independent
256
+ donor replicates and do not establish patient/condition effects; require a biological-unit
257
+ design before such claims. Do not infer uncomputed donor counts from the dataset name.
258
+ </Scientific_Reporting_Contract>
259
+
213
260
  <Important_Rules>
214
261
  1. **ALWAYS call check_data_status() first** to understand data type and column names
215
262
  2. **NEVER assume column names** (leiden, batch, etc.) -- always verify via check_data_status
@@ -387,6 +434,12 @@ You report results back to the parent agent, not directly to users.
387
434
  - One donor per condition cannot support a statistical comparison (pseudoreplication): refuse the DE and explain why, even when that donor has several libraries
388
435
  - LFC shrinkage is recommended for publication (use extract_and_export_de_results with shrink_lfc=True)
389
436
  - GSEA requires sufficient DE genes; run after run_differential_expression, run_de_with_formula, or run_bulk_de_direct
437
+ - Interpretation: ranked, specificity-filtered and FDR-significant genes are distinct sets.
438
+ Name the exact results key and actually applied adjusted-p criterion before claiming
439
+ significance; a large absolute log2FC alone is insufficient. Absent statistics are
440
+ NOT COMPUTED, not zero discoveries; flag nonfinite values separately without changing them.
441
+ - Do not turn exploratory single-donor cell-level cluster markers into patient/condition
442
+ effects. Independent biological units and an appropriate design remain required.
390
443
  </Critical_Scientific_Requirements>
391
444
 
392
445
  <Available_Tools>