factorforge-cds 3.3.1__tar.gz → 3.3.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 (111) hide show
  1. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/PKG-INFO +2 -2
  2. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/README.md +1 -1
  3. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/pyproject.toml +1 -1
  4. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/__init__.py +1 -1
  5. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/analysis/feasibility.py +30 -3
  6. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/cli/main.py +21 -11
  7. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/core/interfaces/optimizer.py +1 -1
  8. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/nbenthamiana_golden_set.json +4 -0
  9. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/__init__.py +1 -1
  10. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/__init__.py +1 -1
  11. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/optimizer.py +14 -6
  12. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/pipeline.py +23 -13
  13. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/rules/reverse_translator.py +81 -10
  14. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge_cds.egg-info/PKG-INFO +2 -2
  15. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge_cds.egg-info/SOURCES.txt +1 -0
  16. factorforge_cds-3.3.2/tests/test_benchmark_cli_output_guard.py +87 -0
  17. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/LICENSE +0 -0
  18. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/setup.cfg +0 -0
  19. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/__main__.py +0 -0
  20. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/analysis/__init__.py +0 -0
  21. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/analysis/metrics.py +0 -0
  22. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/cli/__init__.py +0 -0
  23. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/cli/legacy_cli.py +0 -0
  24. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/core/interfaces/__init__.py +0 -0
  25. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/core/interfaces/exporter.py +0 -0
  26. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/core/interfaces/validator.py +0 -0
  27. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/nbenthamiana_codons.json +0 -0
  28. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/ntabacum_codons.json +0 -0
  29. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/profiles/nbev11_cds_all_derived_codons.json +0 -0
  30. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/profiles/nbev11_cds_all_derived_filtered_stats.json +0 -0
  31. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/profiles/nbev11_cds_all_derived_manifest.json +0 -0
  32. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/profiles/nbev11_cds_hc_derived_codons.json +0 -0
  33. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/profiles/nbev11_cds_hc_derived_filtered_stats.json +0 -0
  34. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/profiles/nbev11_cds_hc_derived_manifest.json +0 -0
  35. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/profiles/qld183_v103_derived_codons.json +0 -0
  36. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/profiles/qld183_v103_derived_manifest.json +0 -0
  37. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/profiles/qld183_v103_filtered_stats.json +0 -0
  38. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/reference/reference_policy_manifest.json +0 -0
  39. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/templates/high_expression.json +0 -0
  40. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/templates/standard_expression.json +0 -0
  41. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/data/wolffia_globosa_codons.json +0 -0
  42. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/database.py +0 -0
  43. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/codon_table_builder.py +0 -0
  44. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/construct_builder.py +0 -0
  45. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/exporter.py +0 -0
  46. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/rules/__init__.py +0 -0
  47. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/rules/domesticator.py +0 -0
  48. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/rules/rule_engine.py +0 -0
  49. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/scoring.py +0 -0
  50. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/scoring_ml.py +0 -0
  51. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/utils.py +0 -0
  52. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/profile/validator.py +0 -0
  53. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/engines/registry.py +0 -0
  54. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/io/__init__.py +0 -0
  55. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/io/fasta.py +0 -0
  56. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/io/validation.py +0 -0
  57. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/protein_risk/__init__.py +0 -0
  58. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/protein_risk/annotate.py +0 -0
  59. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/protein_risk/kd_scale.py +0 -0
  60. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/protein_risk/risk_classifier.py +0 -0
  61. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/protein_risk/sp_predict.py +0 -0
  62. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/protein_risk/tm_predict.py +0 -0
  63. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/registry/__init__.py +0 -0
  64. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/registry/registry_loader.py +0 -0
  65. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/schemas/__init__.py +0 -0
  66. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/schemas/design_package.py +0 -0
  67. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/schemas/design_package.schema.json +0 -0
  68. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/utils/__init__.py +0 -0
  69. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/utils/construct_id.py +0 -0
  70. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/utils/exceptions.py +0 -0
  71. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/utils/restriction_sites.py +0 -0
  72. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/utils/sequence_validator.py +0 -0
  73. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/utils/validation.py +0 -0
  74. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/validation/__init__.py +0 -0
  75. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/validation/cli.py +0 -0
  76. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/validation/package_generator.py +0 -0
  77. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/validation_registry.py +0 -0
  78. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge/validation_report.py +0 -0
  79. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge_cds.egg-info/dependency_links.txt +0 -0
  80. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge_cds.egg-info/entry_points.txt +0 -0
  81. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge_cds.egg-info/requires.txt +0 -0
  82. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/src/factorforge_cds.egg-info/top_level.txt +0 -0
  83. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_baselines.py +0 -0
  84. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_benchmark_codon_table_metadata.py +0 -0
  85. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_benchmark_regression.py +0 -0
  86. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_benchmark_scoring.py +0 -0
  87. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_benchmark_smoke.py +0 -0
  88. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_cai.py +0 -0
  89. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_codon_table_manifest.py +0 -0
  90. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_database.py +0 -0
  91. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_design_package_schema.py +0 -0
  92. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_design_package_semantics.py +0 -0
  93. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_design_package_serialization.py +0 -0
  94. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_docs_consistency.py +0 -0
  95. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_fasta_io.py +0 -0
  96. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_gc_content.py +0 -0
  97. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_host_profile_metadata.py +0 -0
  98. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_iupac_validation.py +0 -0
  99. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_legacy_cli.py +0 -0
  100. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_no_raw_sequence_logging.py +0 -0
  101. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_openbio_missing_metric_contract.py +0 -0
  102. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_parameter_registry.py +0 -0
  103. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_protein_risk.py +0 -0
  104. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_registry_production_sync.py +0 -0
  105. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_restriction_sites.py +0 -0
  106. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_sequence_validator.py +0 -0
  107. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_translation_integrity.py +0 -0
  108. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_validation_contract_compat.py +0 -0
  109. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_validation_registry.py +0 -0
  110. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_validation_report.py +0 -0
  111. {factorforge_cds-3.3.1 → factorforge_cds-3.3.2}/tests/test_worked_example.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: factorforge-cds
3
- Version: 3.3.1
3
+ Version: 3.3.2
4
4
  Summary: FactorForge - open-source CDS design and pre-synthesis sequence review by Eijex.
5
5
  Author-email: Eijex <eijex.lab@gmail.com>
6
6
  License-Expression: AGPL-3.0-only
@@ -93,7 +93,7 @@ FactorForge outputs are **in-silico only** and have not been experimentally vali
93
93
  ## Citing
94
94
 
95
95
  ```
96
- FactorForge v3.3.1 (2026). Open-source constraint-based CDS design and sequence review.
96
+ FactorForge v3.3.2 (2026). Open-source constraint-based CDS design and sequence review.
97
97
  Eijex. https://github.com/eijex/factorforge-cds
98
98
  ```
99
99
 
@@ -60,7 +60,7 @@ FactorForge outputs are **in-silico only** and have not been experimentally vali
60
60
  ## Citing
61
61
 
62
62
  ```
63
- FactorForge v3.3.1 (2026). Open-source constraint-based CDS design and sequence review.
63
+ FactorForge v3.3.2 (2026). Open-source constraint-based CDS design and sequence review.
64
64
  Eijex. https://github.com/eijex/factorforge-cds
65
65
  ```
66
66
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "factorforge-cds"
7
- version = "3.3.1"
7
+ version = "3.3.2"
8
8
  description = "FactorForge - open-source CDS design and pre-synthesis sequence review by Eijex."
9
9
  readme = "README.md"
10
10
  license = "AGPL-3.0-only"
@@ -4,7 +4,7 @@ FactorForge - Codon Optimization Platform
4
4
  profile: constraint-aware rule/profile engine
5
5
  """
6
6
 
7
- __version__ = "3.3.1"
7
+ __version__ = "3.3.2"
8
8
  __author__ = "Eijex"
9
9
 
10
10
  # Auto-register engines (safe when running from source tree)
@@ -51,6 +51,7 @@ def _cai_from_log(log_sum: float, codon_count: int) -> float:
51
51
  def _candidate_summary(
52
52
  dna_sequence: str | None,
53
53
  codon_weights: dict[str, float],
54
+ cai_authority: dict[str, Any],
54
55
  ) -> dict[str, Any] | None:
55
56
  if dna_sequence is None:
56
57
  return None
@@ -59,6 +60,7 @@ def _candidate_summary(
59
60
  return {
60
61
  "dna_sequence": dna_sequence,
61
62
  "cai": calculate_cai(dna_sequence, codon_weights),
63
+ "cai_authority": dict(cai_authority),
62
64
  "gc": calculate_gc(dna_sequence),
63
65
  "first_region_gc": calculate_first_region_gc(dna_sequence),
64
66
  "gc_window_min": min(window_values) if window_values else 0.0,
@@ -66,6 +68,20 @@ def _candidate_summary(
66
68
  }
67
69
 
68
70
 
71
+ def _build_cai_authority(codon_reference_id: str | None) -> dict[str, Any]:
72
+ if codon_reference_id is None:
73
+ return {
74
+ "reference_id": None,
75
+ "reference_role": "cai_evaluation",
76
+ "reference_relationship": "unresolved",
77
+ }
78
+ return {
79
+ "reference_id": codon_reference_id,
80
+ "reference_role": "cai_evaluation",
81
+ "reference_relationship": "same_as_generation_reference",
82
+ }
83
+
84
+
69
85
  def _best_gc_under_gc_range(
70
86
  states: dict[int, float],
71
87
  protein_length: int,
@@ -104,6 +120,7 @@ def analyze_feasibility(
104
120
  target_gc_low: float = DEFAULT_GC_LOW,
105
121
  target_gc_high: float = DEFAULT_GC_HIGH,
106
122
  gc_ranges: list[tuple[float, float]] | None = None,
123
+ codon_reference_id: str | None = None,
107
124
  ) -> dict[str, Any]:
108
125
  """Compute exact CAI/GC feasibility over synonymous codon choices.
109
126
 
@@ -160,6 +177,7 @@ def analyze_feasibility(
160
177
 
161
178
  protein_length = len(protein)
162
179
  total_bases = protein_length * 3
180
+ cai_authority = _build_cai_authority(codon_reference_id)
163
181
  best_any_gc = max(states, key=lambda gc_count: states[gc_count])
164
182
  best_any_log_sum = states[best_any_gc]
165
183
  best_any_sequence = _reconstruct_sequence(backrefs, best_any_gc)
@@ -180,7 +198,7 @@ def analyze_feasibility(
180
198
  range_results[key] = {
181
199
  "feasible": True,
182
200
  "max_cai": _cai_from_log(states[gc_count], protein_length),
183
- "best_candidate": _candidate_summary(sequence, codon_weights),
201
+ "best_candidate": _candidate_summary(sequence, codon_weights, cai_authority),
184
202
  }
185
203
 
186
204
  target_cai_possible = (
@@ -192,8 +210,13 @@ def analyze_feasibility(
192
210
  "protein_length": protein_length,
193
211
  "minimum_possible_gc": (min_gc_count / total_bases) * 100.0,
194
212
  "maximum_possible_gc": (max_gc_count / total_bases) * 100.0,
213
+ "cai_authority": dict(cai_authority),
195
214
  "maximum_achievable_cai_without_gc": _cai_from_log(best_any_log_sum, protein_length),
196
- "best_candidate_without_gc": _candidate_summary(best_any_sequence, codon_weights),
215
+ "best_candidate_without_gc": _candidate_summary(
216
+ best_any_sequence,
217
+ codon_weights,
218
+ cai_authority,
219
+ ),
197
220
  "ranges": range_results,
198
221
  "target": {
199
222
  "cai": target_cai,
@@ -206,7 +229,11 @@ def analyze_feasibility(
206
229
  else None
207
230
  ),
208
231
  "best_candidate": (
209
- _candidate_summary(_reconstruct_sequence(backrefs, target_gc_count), codon_weights)
232
+ _candidate_summary(
233
+ _reconstruct_sequence(backrefs, target_gc_count),
234
+ codon_weights,
235
+ cai_authority,
236
+ )
210
237
  if target_gc_count is not None
211
238
  else None
212
239
  ),
@@ -154,6 +154,7 @@ def _build_dp_result(
154
154
  gc_max: float,
155
155
  cai_target: float = DEFAULT_CAI_TARGET,
156
156
  codon_table_path: Path | None = None,
157
+ codon_reference_id: str | None = None,
157
158
  ):
158
159
  """Run the constraint-based DP feasibility engine for a single protein sequence."""
159
160
  if objective != "feasibility_best":
@@ -171,6 +172,7 @@ def _build_dp_result(
171
172
  target_cai=cai_target,
172
173
  target_gc_low=gc_min,
173
174
  target_gc_high=gc_max,
175
+ codon_reference_id=codon_reference_id,
174
176
  )
175
177
  best = result["target"]["best_candidate"]
176
178
  feasible = best is not None
@@ -222,7 +224,10 @@ def _format_profile_fasta(sequence_id: str, profile: str, result) -> str:
222
224
  cai = float(result.metrics.get("cai", 0.0))
223
225
  gc = float(result.metrics.get("gc_percent", result.metrics.get("gc_content", 0.0)))
224
226
  score = float(result.metrics.get("score", 0.0))
225
- header = f">{sequence_id}|profile={profile}|cai={cai:.3f}|gc={gc:.2f}|score={score:.3f}"
227
+ header = (
228
+ f">{sequence_id}|engine=profile|profile={profile}|"
229
+ f"cai={cai:.3f}|gc={gc:.2f}|score={score:.3f}"
230
+ )
226
231
  return f"{header}\n{_wrap_sequence(result.sequence)}\n"
227
232
 
228
233
 
@@ -399,7 +404,10 @@ def optimize(
399
404
  if reference_table_path is not None:
400
405
  from factorforge.engines.profile.optimizer import RuleBasedOptimizer
401
406
 
402
- optimizer = RuleBasedOptimizer(codon_table_path=str(reference_table_path))
407
+ optimizer = RuleBasedOptimizer(
408
+ codon_table_path=str(reference_table_path),
409
+ generation_reference_id=reference_id,
410
+ )
403
411
  else:
404
412
  optimizer = EngineRegistry.get("profile")
405
413
  profile_results = []
@@ -436,7 +444,10 @@ def optimize(
436
444
  if reference_table_path is not None and engine == "profile":
437
445
  from factorforge.engines.profile.optimizer import RuleBasedOptimizer
438
446
 
439
- optimizer = RuleBasedOptimizer(codon_table_path=str(reference_table_path))
447
+ optimizer = RuleBasedOptimizer(
448
+ codon_table_path=str(reference_table_path),
449
+ generation_reference_id=reference_id,
450
+ )
440
451
  else:
441
452
  optimizer = EngineRegistry.get(engine)
442
453
  payload = [{"id": seq_id, "sequence": seq} for seq_id, seq in fasta_records]
@@ -465,14 +476,9 @@ def optimize(
465
476
  combined_fasta = []
466
477
  for idx, result in enumerate(results):
467
478
  seq_id = payload[idx]["id"]
468
- cai = result.metrics.get("cai", 0.0)
469
- gc = result.metrics.get("gc_percent", result.metrics.get("gc_content", 0.0))
470
- score = result.metrics.get("score", 0.0)
471
- header = (
472
- f">{seq_id}|profile={profile}|cai={float(cai):.3f}|"
473
- f"gc={float(gc):.2f}|score={float(score):.3f}"
479
+ combined_fasta.append(
480
+ _format_profile_fasta(seq_id, profile, result).rstrip()
474
481
  )
475
- combined_fasta.append(f"{header}\n{_wrap_sequence(result.sequence)}")
476
482
  out_content = "\n".join(combined_fasta) + "\n"
477
483
 
478
484
  if output:
@@ -497,6 +503,7 @@ def optimize(
497
503
  gc_max=gc_max,
498
504
  cai_target=cai_target,
499
505
  codon_table_path=reference_table_path,
506
+ codon_reference_id=reference_id,
500
507
  )
501
508
  dna_sequence = best["dna_sequence"]
502
509
  cai = float(best["cai"])
@@ -566,7 +573,10 @@ def optimize(
566
573
  if reference_table_path is not None and engine == "profile":
567
574
  from factorforge.engines.profile.optimizer import RuleBasedOptimizer
568
575
 
569
- optimizer = RuleBasedOptimizer(codon_table_path=str(reference_table_path))
576
+ optimizer = RuleBasedOptimizer(
577
+ codon_table_path=str(reference_table_path),
578
+ generation_reference_id=reference_id,
579
+ )
570
580
  else:
571
581
  optimizer = EngineRegistry.get(engine)
572
582
 
@@ -16,7 +16,7 @@ class OptimizationResult:
16
16
  def __init__(
17
17
  self,
18
18
  sequence: str,
19
- metrics: dict[str, float],
19
+ metrics: dict[str, Any],
20
20
  metadata: dict[str, Any] | None = None,
21
21
  ) -> None:
22
22
  self.sequence = sequence
@@ -1,6 +1,10 @@
1
1
  {
2
2
  "organism": "Nicotiana benthamiana",
3
+ "reference_id": "nbenthamiana_golden_set_v1",
4
+ "reference_role": "cai_evaluation",
5
+ "reference_version": "1",
3
6
  "source": "N. benthamiana high-expression CDS frequencies (70%, Kazusa CodonUsage DB / NCBI) blended with RNA-seq expression-weighted codon frequencies (30%, Grosse-Holz et al. 2018 Plant Biotechnology Journal; Prudhomme et al. 2024 Plant Biotechnology Journal / PRIDE PXD042916)",
7
+ "source_basis": "N. benthamiana high-expression CDS frequencies (70%, Kazusa CodonUsage DB / NCBI) blended with RNA-seq expression-weighted codon frequencies (30%, Grosse-Holz et al. 2018 Plant Biotechnology Journal; Prudhomme et al. 2024 Plant Biotechnology Journal / PRIDE PXD042916)",
4
8
  "description": "Codon usage frequencies weighted toward highly expressed N. benthamiana genes (RuBisCO, ribosomal proteins, histones). Used as CAI reference weights per Sharp & Li (1987). All source data is publicly available.",
5
9
  "blend_ratio": 0.7,
6
10
  "optimal_gc_content": 42.5,
@@ -13,7 +13,7 @@ def register_builtin_engines() -> None:
13
13
  "profile",
14
14
  RuleBasedOptimizer,
15
15
  metadata={
16
- "version": "3.3.1",
16
+ "version": "3.3.2",
17
17
  "engine_type": "profile_rule_based",
18
18
  "role": "stable_profile_engine",
19
19
  "stable": True,
@@ -5,7 +5,7 @@ Production system (2026)
5
5
  Plant-specific rule-based optimization
6
6
  """
7
7
 
8
- __version__ = "3.3.1"
8
+ __version__ = "3.3.2"
9
9
 
10
10
  from .optimizer import RuleBasedOptimizer
11
11
  from .pipeline import OptimizationPipeline
@@ -20,9 +20,13 @@ class RuleBasedOptimizer(OptimizerEngine):
20
20
  """Profile-based rule optimization engine."""
21
21
 
22
22
  name = "Profile-based"
23
- version = "3.3.1"
23
+ version = "3.3.2"
24
24
 
25
- def __init__(self, codon_table_path: str | None = None) -> None:
25
+ def __init__(
26
+ self,
27
+ codon_table_path: str | None = None,
28
+ generation_reference_id: str | None = None,
29
+ ) -> None:
26
30
  """
27
31
  Args:
28
32
  codon_table_path: Optional path to a codon usage table JSON used for
@@ -31,6 +35,8 @@ class RuleBasedOptimizer(OptimizerEngine):
31
35
  product `optimize` CLI and the registered engine ever take, so
32
36
  their behavior is unchanged. Injection is used by the benchmark
33
37
  harness to drive design with alternative source-profile tables.
38
+ generation_reference_id: Explicit manifest reference id when the
39
+ injected design table is also the CAI evaluation reference.
34
40
  """
35
41
  self.validator = InputValidator()
36
42
  self._codon_table_path = codon_table_path
@@ -43,6 +49,8 @@ class RuleBasedOptimizer(OptimizerEngine):
43
49
  self.translator = ReverseTranslator(
44
50
  codon_table_path=codon_table_path,
45
51
  golden_set_path=codon_table_path,
52
+ generation_reference_id=generation_reference_id,
53
+ _allow_golden_set_metadata_fallback=codon_table_path is not None,
46
54
  )
47
55
  self.rule_engine = RuleEngine()
48
56
  self.exporter = SequenceExporter()
@@ -127,7 +135,8 @@ class RuleBasedOptimizer(OptimizerEngine):
127
135
  candidates = [{"sequence": optimized_dna, "cai": cai, "gc": gc, "score": score}]
128
136
  else:
129
137
  translate_kwargs = {
130
- k: v for k, v in kwargs.items()
138
+ k: v
139
+ for k, v in kwargs.items()
131
140
  if k not in ("scan_mode", "scan_include", "scan_exclude")
132
141
  }
133
142
  candidates = translator.generate_candidates(
@@ -151,6 +160,7 @@ class RuleBasedOptimizer(OptimizerEngine):
151
160
  # 5. Build result
152
161
  metrics = {
153
162
  "cai": candidates[0]["cai"],
163
+ "cai_authority": dict(translator.cai_authority),
154
164
  # Keep both names for compatibility across existing tests/callers.
155
165
  "gc_content": candidates[0]["gc"],
156
166
  "gc_percent": candidates[0]["gc"],
@@ -165,9 +175,7 @@ class RuleBasedOptimizer(OptimizerEngine):
165
175
  metrics.update(
166
176
  {
167
177
  "gc_target_reached": (
168
- requested_gc_min_percent
169
- <= achieved_gc_percent
170
- <= requested_gc_max_percent
178
+ requested_gc_min_percent <= achieved_gc_percent <= requested_gc_max_percent
171
179
  ),
172
180
  "requested_gc_min_percent": requested_gc_min_percent,
173
181
  "requested_gc_max_percent": requested_gc_max_percent,
@@ -187,16 +187,25 @@ class OptimizationPipeline:
187
187
  f"Unknown profile: {effective_profile}. Supported profiles: {supported}"
188
188
  ) from exc
189
189
 
190
+ candidate_metrics: dict[str, Any]
190
191
  if seq_type == "dna":
191
192
  optimized_dna = processed
192
193
  expected_protein = translate_dna(processed).rstrip("*")
193
194
  cai = translator.calculate_cai(optimized_dna)
194
195
  gc = translator.calculate_gc_content(optimized_dna)
195
196
  score = calculate_composite_score(
196
- cai=cai, gc=gc, sequence=optimized_dna, profile=effective_profile,
197
+ cai=cai,
198
+ gc=gc,
199
+ sequence=optimized_dna,
200
+ profile=effective_profile,
197
201
  host=effective_host,
198
202
  )
199
- candidate_metrics = {"cai": cai, "gc": gc, "score": score}
203
+ candidate_metrics = {
204
+ "cai": cai,
205
+ "cai_authority": dict(translator.cai_authority),
206
+ "gc": gc,
207
+ "score": score,
208
+ }
200
209
  else:
201
210
  expected_protein = processed.rstrip("*")
202
211
  logger.debug(f"Generating candidates with profile: {opt_profile.value}")
@@ -207,6 +216,7 @@ class OptimizationPipeline:
207
216
  optimized_dna = candidates[0]["sequence"]
208
217
  candidate_metrics = {
209
218
  "cai": candidates[0]["cai"],
219
+ "cai_authority": dict(translator.cai_authority),
210
220
  "gc": candidates[0]["gc"],
211
221
  "score": candidates[0]["score"],
212
222
  }
@@ -216,9 +226,7 @@ class OptimizationPipeline:
216
226
  )
217
227
 
218
228
  # Fast pre-check avoids an expensive full rule scan before PolyA fixing.
219
- has_polya_signal = any(
220
- pattern in optimized_dna for pattern in rule_engine.POLYA_PATTERNS
221
- )
229
+ has_polya_signal = any(pattern in optimized_dna for pattern in rule_engine.POLYA_PATTERNS)
222
230
  if has_polya_signal:
223
231
  logger.debug("Potential PolyA signal detected; attempting iterative fix")
224
232
  polya_fix = rule_engine.fix_polya_iterative(optimized_dna)
@@ -239,15 +247,19 @@ class OptimizationPipeline:
239
247
  dinu_fix = rule_engine.fix_dinucleotides(optimized_dna, mode="balanced")
240
248
  if dinu_fix["success"]:
241
249
  optimized_dna = dinu_fix["modified_seq"]
242
- candidate_metrics["cai"] = round(translator.calculate_cai(optimized_dna), 4)
243
- candidate_metrics["gc"] = translator.calculate_gc_content(optimized_dna)
244
- candidate_metrics["score"] = calculate_composite_score(
245
- cai=candidate_metrics["cai"],
246
- gc=candidate_metrics["gc"],
250
+ cai_after_dinucleotide_fix = round(translator.calculate_cai(optimized_dna), 4)
251
+ gc_after_dinucleotide_fix = translator.calculate_gc_content(optimized_dna)
252
+ score_after_dinucleotide_fix = calculate_composite_score(
253
+ cai=cai_after_dinucleotide_fix,
254
+ gc=gc_after_dinucleotide_fix,
247
255
  sequence=optimized_dna,
248
256
  profile=effective_profile,
249
257
  host=effective_host,
250
258
  )
259
+ candidate_metrics["cai"] = cai_after_dinucleotide_fix
260
+ candidate_metrics["cai_authority"] = dict(translator.cai_authority)
261
+ candidate_metrics["gc"] = gc_after_dinucleotide_fix
262
+ candidate_metrics["score"] = score_after_dinucleotide_fix
251
263
  logger.info(
252
264
  f"Dinucleotide reduction [{dinu_fix['mode']}]: "
253
265
  f"{dinu_fix['initial_count']} -> "
@@ -287,9 +299,7 @@ class OptimizationPipeline:
287
299
  template_name = construct_template or self.construct_template
288
300
  if template_name:
289
301
  if self.construct_builder is None:
290
- template_dir = (
291
- self.template_dir or get_data_path() / "templates"
292
- )
302
+ template_dir = self.template_dir or get_data_path() / "templates"
293
303
  self.construct_builder = ConstructBuilder(template_dir)
294
304
  construct_record = self.construct_builder.generate_construct(
295
305
  gene_sequence=domesticated_sequence,
@@ -13,7 +13,7 @@ import random
13
13
  import secrets
14
14
  from enum import Enum
15
15
  from pathlib import Path
16
- from typing import Any, cast
16
+ from typing import Any, TypedDict, cast
17
17
 
18
18
  from factorforge.engines.profile.scoring import (
19
19
  calculate_composite_score,
@@ -23,7 +23,6 @@ from factorforge.engines.profile.utils import (
23
23
  build_aa_to_codons_map,
24
24
  calculate_gc,
25
25
  get_data_path,
26
- load_golden_set,
27
26
  resolve_host_codon_table_path,
28
27
  )
29
28
  from factorforge.utils.exceptions import EmptyCandidateError
@@ -31,6 +30,16 @@ from factorforge.utils.exceptions import EmptyCandidateError
31
30
  logger = logging.getLogger(__name__)
32
31
 
33
32
 
33
+ class CaiAuthority(TypedDict, total=False):
34
+ """JSON-serializable CAI reference authority metadata."""
35
+
36
+ reference_id: str | None
37
+ reference_role: str
38
+ reference_version: str | None
39
+ reference_relationship: str
40
+ fallback_used: bool
41
+
42
+
34
43
  class OptimizationProfile(Enum):
35
44
  """Optimization profile"""
36
45
 
@@ -58,6 +67,8 @@ class ReverseTranslator:
58
67
  codon_table_path: str | Path | None = None,
59
68
  golden_set_path: str | Path | None = None,
60
69
  host: str = "nbenthamiana",
70
+ generation_reference_id: str | None = None,
71
+ _allow_golden_set_metadata_fallback: bool = False,
61
72
  ) -> None:
62
73
  """
63
74
  Args:
@@ -65,6 +76,8 @@ class ReverseTranslator:
65
76
  golden_set_path: Path to golden set JSON for CAI reference weights.
66
77
  If None, attempts to load default golden set.
67
78
  host: Host codon table name used when codon_table_path is not provided.
79
+ generation_reference_id: Explicit manifest reference id when the CAI
80
+ reference is intentionally the same table used for generation.
68
81
  """
69
82
  self.host = host
70
83
  if codon_table_path is None:
@@ -76,13 +89,38 @@ class ReverseTranslator:
76
89
  self.aa_to_codons: dict[str, list[tuple[str, float]]] = self._build_aa_to_codons_map()
77
90
 
78
91
  # Load golden set for CAI reference weights
79
- if golden_set_path is not None:
80
- self.golden_set_table: dict[str, Any] = self._load_codon_table(golden_set_path)
81
- else:
82
- try:
83
- self.golden_set_table = load_golden_set()
84
- except (FileNotFoundError, json.JSONDecodeError):
85
- self.golden_set_table = self.codon_table
92
+ try:
93
+ if golden_set_path is not None:
94
+ self.golden_set_table = self._load_codon_table(golden_set_path)
95
+ else:
96
+ self.golden_set_table = self._load_codon_table(
97
+ get_data_path() / "nbenthamiana_golden_set.json"
98
+ )
99
+ if generation_reference_id is not None:
100
+ self.cai_authority = self._build_generation_reference_cai_authority(
101
+ generation_reference_id
102
+ )
103
+ else:
104
+ try:
105
+ self.cai_authority = self._build_cai_authority(self.golden_set_table)
106
+ except ValueError:
107
+ if not _allow_golden_set_metadata_fallback:
108
+ raise
109
+ self.golden_set_table = self.codon_table
110
+ self.cai_authority = {
111
+ "reference_id": self.codon_table.get("reference_id"),
112
+ "reference_role": "cai_evaluation",
113
+ "reference_relationship": "fallback_to_generation_reference",
114
+ "fallback_used": True,
115
+ }
116
+ except (FileNotFoundError, json.JSONDecodeError):
117
+ self.golden_set_table = self.codon_table
118
+ self.cai_authority = {
119
+ "reference_id": self.codon_table.get("reference_id"),
120
+ "reference_role": "cai_evaluation",
121
+ "reference_relationship": "fallback_to_generation_reference",
122
+ "fallback_used": True,
123
+ }
86
124
 
87
125
  # Pre-compute relative adaptiveness weights from golden set (Sharp & Li 1987)
88
126
  self.golden_ref_weights: dict[str, float] = self._build_ref_weights(self.golden_set_table)
@@ -132,6 +170,37 @@ class ReverseTranslator:
132
170
  with open(path, "r", encoding="utf-8") as f:
133
171
  return cast(dict[str, Any], json.load(f))
134
172
 
173
+ @staticmethod
174
+ def _build_cai_authority(golden_set_table: dict[str, Any]) -> CaiAuthority:
175
+ """Validate and expose golden-set CAI authority metadata."""
176
+ required_fields = ("reference_id", "reference_role", "reference_version")
177
+ missing = [field for field in required_fields if not golden_set_table.get(field)]
178
+ if missing:
179
+ raise ValueError(
180
+ "Golden-set CAI authority metadata is missing required field(s): "
181
+ + ", ".join(missing)
182
+ )
183
+ if golden_set_table["reference_role"] != "cai_evaluation":
184
+ raise ValueError("Golden-set CAI authority reference_role must be 'cai_evaluation'.")
185
+
186
+ return {
187
+ "reference_id": str(golden_set_table["reference_id"]),
188
+ "reference_role": "cai_evaluation",
189
+ "reference_version": str(golden_set_table["reference_version"]),
190
+ "reference_relationship": "distinct_from_generation_reference",
191
+ "fallback_used": False,
192
+ }
193
+
194
+ @staticmethod
195
+ def _build_generation_reference_cai_authority(reference_id: str) -> CaiAuthority:
196
+ """Expose explicit generation-reference reuse as intentional, not fallback."""
197
+ return {
198
+ "reference_id": reference_id,
199
+ "reference_role": "cai_evaluation",
200
+ "reference_relationship": "same_as_generation_reference",
201
+ "fallback_used": False,
202
+ }
203
+
135
204
  def _build_aa_to_codons_map(self) -> dict[str, list[tuple[str, float]]]:
136
205
  """
137
206
  Build amino-acid-to-codons map
@@ -520,7 +589,9 @@ class ReverseTranslator:
520
589
  last_seq = ""
521
590
 
522
591
  balanced_kwargs = {
523
- k: v for k, v in kwargs.items() if k in ("target_gc_min", "target_gc_max", "max_gc_attempts")
592
+ k: v
593
+ for k, v in kwargs.items()
594
+ if k in ("target_gc_min", "target_gc_max", "max_gc_attempts")
524
595
  }
525
596
  balanced_kwargs["preferred_ratio"] = 0.6
526
597
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: factorforge-cds
3
- Version: 3.3.1
3
+ Version: 3.3.2
4
4
  Summary: FactorForge - open-source CDS design and pre-synthesis sequence review by Eijex.
5
5
  Author-email: Eijex <eijex.lab@gmail.com>
6
6
  License-Expression: AGPL-3.0-only
@@ -93,7 +93,7 @@ FactorForge outputs are **in-silico only** and have not been experimentally vali
93
93
  ## Citing
94
94
 
95
95
  ```
96
- FactorForge v3.3.1 (2026). Open-source constraint-based CDS design and sequence review.
96
+ FactorForge v3.3.2 (2026). Open-source constraint-based CDS design and sequence review.
97
97
  Eijex. https://github.com/eijex/factorforge-cds
98
98
  ```
99
99
 
@@ -78,6 +78,7 @@ src/factorforge_cds.egg-info/entry_points.txt
78
78
  src/factorforge_cds.egg-info/requires.txt
79
79
  src/factorforge_cds.egg-info/top_level.txt
80
80
  tests/test_baselines.py
81
+ tests/test_benchmark_cli_output_guard.py
81
82
  tests/test_benchmark_codon_table_metadata.py
82
83
  tests/test_benchmark_regression.py
83
84
  tests/test_benchmark_scoring.py
@@ -0,0 +1,87 @@
1
+ """CLI output-directory safety tests for the benchmark runner."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ import benchmarks.run_benchmark as run_benchmark
12
+
13
+
14
+ def _invoke_main(monkeypatch: pytest.MonkeyPatch, args: list[str], fake_run) -> None:
15
+ monkeypatch.setattr(sys, "argv", ["run_benchmark.py", *args])
16
+ monkeypatch.setattr(run_benchmark, "run", fake_run)
17
+ run_benchmark.main()
18
+
19
+
20
+ def test_cli_default_output_dir_stays_v320(monkeypatch: pytest.MonkeyPatch) -> None:
21
+ captured: dict[str, Path] = {}
22
+
23
+ def fake_run(**kwargs) -> None:
24
+ captured["out_csv"] = kwargs["out_csv"]
25
+ captured["out_md"] = kwargs["out_md"]
26
+
27
+ _invoke_main(monkeypatch, ["--force"], fake_run)
28
+
29
+ assert captured["out_csv"] == run_benchmark.DEFAULT_RESULTS_DIR.resolve() / "benchmark_results.csv"
30
+ assert captured["out_md"] == run_benchmark.DEFAULT_RESULTS_DIR.resolve() / "benchmark_summary.md"
31
+
32
+
33
+ def test_cli_refuses_existing_results_without_force(
34
+ monkeypatch: pytest.MonkeyPatch,
35
+ tmp_path: Path,
36
+ ) -> None:
37
+ output_dir = tmp_path / "existing"
38
+ output_dir.mkdir()
39
+ existing_csv = output_dir / "benchmark_results.csv"
40
+ existing_json = output_dir / "benchmark_summary.json"
41
+ existing_csv.write_text("do-not-overwrite\n", encoding="utf-8")
42
+ existing_json.write_text('{"status": "do-not-overwrite"}\n', encoding="utf-8")
43
+ before_csv_hash = hashlib.sha256(existing_csv.read_bytes()).hexdigest()
44
+ before_json_hash = hashlib.sha256(existing_json.read_bytes()).hexdigest()
45
+ before_csv_mtime = existing_csv.stat().st_mtime_ns
46
+ before_json_mtime = existing_json.stat().st_mtime_ns
47
+
48
+ def fail_run(**_kwargs) -> None:
49
+ raise AssertionError("run() must not be called when overwrite guard fails")
50
+
51
+ with pytest.raises(SystemExit) as exc_info:
52
+ _invoke_main(monkeypatch, ["--out-dir", str(output_dir)], fail_run)
53
+
54
+ message = str(exc_info.value)
55
+ assert "Pass --force" in message
56
+ assert "benchmark_results.csv" in message
57
+ assert "benchmark_summary.json" in message
58
+ assert hashlib.sha256(existing_csv.read_bytes()).hexdigest() == before_csv_hash
59
+ assert hashlib.sha256(existing_json.read_bytes()).hexdigest() == before_json_hash
60
+ assert existing_csv.stat().st_mtime_ns == before_csv_mtime
61
+ assert existing_json.stat().st_mtime_ns == before_json_mtime
62
+
63
+
64
+ def test_cli_force_allows_existing_results_overwrite(
65
+ monkeypatch: pytest.MonkeyPatch,
66
+ tmp_path: Path,
67
+ ) -> None:
68
+ output_dir = tmp_path / "existing"
69
+ output_dir.mkdir()
70
+ (output_dir / "benchmark_results.csv").write_text("old\n", encoding="utf-8")
71
+ (output_dir / "benchmark_summary.json").write_text('{"status": "old"}\n', encoding="utf-8")
72
+
73
+ def fake_run(**kwargs) -> None:
74
+ kwargs["out_csv"].write_text("new\n", encoding="utf-8")
75
+ kwargs["out_md"].write_text("# new\n", encoding="utf-8")
76
+ (kwargs["out_md"].parent / "benchmark_summary.json").write_text(
77
+ '{"status": "new"}\n',
78
+ encoding="utf-8",
79
+ )
80
+
81
+ _invoke_main(monkeypatch, ["--out-dir", str(output_dir), "--force"], fake_run)
82
+
83
+ assert (output_dir / "benchmark_results.csv").read_text(encoding="utf-8") == "new\n"
84
+ assert (output_dir / "benchmark_summary.md").read_text(encoding="utf-8") == "# new\n"
85
+ assert (output_dir / "benchmark_summary.json").read_text(encoding="utf-8") == (
86
+ '{"status": "new"}\n'
87
+ )
File without changes